Compare commits
3 Commits
main
...
c5036228ea
| Author | SHA1 | Date | |
|---|---|---|---|
| c5036228ea | |||
| 3c80edd3c2 | |||
| 61aa476ae8 |
@@ -17,6 +17,15 @@ If the target includes multiple matching file types, use all matching skills.
|
|||||||
If the target is a diff, choose skills from the changed files.
|
If the target is a diff, choose skills from the changed files.
|
||||||
Do not use a style skill when no reviewed file matches it.
|
Do not use a style skill when no reviewed file matches it.
|
||||||
|
|
||||||
|
For every reviewed `*.nix` file, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bazel run //src/natu:natu -- <reviewed-nix-paths>
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass every reviewed Nix path to Natu.
|
||||||
|
Report every Natu diagnostic as an actionable finding.
|
||||||
|
|
||||||
Report only actionable findings.
|
Report only actionable findings.
|
||||||
Order findings by severity.
|
Order findings by severity.
|
||||||
Do not report preferences unless a loaded rule states them.
|
Do not report preferences unless a loaded rule states them.
|
||||||
@@ -36,6 +45,7 @@ Keep the code excerpt to one to three lines when possible.
|
|||||||
Use a longer excerpt only when the violation needs surrounding context.
|
Use a longer excerpt only when the violation needs surrounding context.
|
||||||
Use `...` to omit unrelated lines from a multi-line excerpt.
|
Use `...` to omit unrelated lines from a multi-line excerpt.
|
||||||
Use a style-skill rule citation in the form `skill::rule-file:line`.
|
Use a style-skill rule citation in the form `skill::rule-file:line`.
|
||||||
|
Cite Natu diagnostics as `natu::<rule-id>`.
|
||||||
Make the fix direct.
|
Make the fix direct.
|
||||||
Do not include alternatives when one fix best follows the cited rule.
|
Do not include alternatives when one fix best follows the cited rule.
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
description: Summarize an agent response
|
|
||||||
argument-hint: "[source request]"
|
|
||||||
---
|
|
||||||
|
|
||||||
Summarize `${ARGUMENTS:-the preceding agent response}`.
|
|
||||||
Keep the summary compact.
|
|
||||||
Use no fixed format.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../../src/skills/agent-writing-guide
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../../src/skills/conventional-commits
|
|
||||||
1
.pi/skills/craft-skill
Symbolic link
1
.pi/skills/craft-skill
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../src/skills/craft-skill
|
||||||
@@ -1 +0,0 @@
|
|||||||
../../src/skills/retrospect
|
|
||||||
1
.pi/skills/to-document
Symbolic link
1
.pi/skills/to-document
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../src/skills/to-document
|
||||||
28
AGENTS.md
28
AGENTS.md
@@ -3,6 +3,7 @@
|
|||||||
## Commit convention
|
## Commit convention
|
||||||
|
|
||||||
- Use Conventional Commits for all commit messages.
|
- Use Conventional Commits for all commit messages.
|
||||||
|
Before writing a commit message, read `docs/conventional-commits.md`.
|
||||||
- Keep commit messages about the Git history, not the forge workflow.
|
- Keep commit messages about the Git history, not the forge workflow.
|
||||||
Include external references only when they are stable context for the change.
|
Include external references only when they are stable context for the change.
|
||||||
- When writing commit messages, NEVER auto-add your agent name as co-author.
|
- When writing commit messages, NEVER auto-add your agent name as co-author.
|
||||||
@@ -48,8 +49,6 @@
|
|||||||
- NEVER ask the user a question using the `AskUserQuestion` tool.
|
- NEVER ask the user a question using the `AskUserQuestion` tool.
|
||||||
Ask in plain prose, in your own message, instead, with no exceptions.
|
Ask in plain prose, in your own message, instead, with no exceptions.
|
||||||
This overrides any default instruction to use it.
|
This overrides any default instruction to use it.
|
||||||
- For review requests, inspect the requested material and report findings.
|
|
||||||
Edit files after the operator accepts a proposed change.
|
|
||||||
- Never manually modify `CHANGELOG.md` files or files marked as auto-generated.
|
- Never manually modify `CHANGELOG.md` files or files marked as auto-generated.
|
||||||
Detect auto-generated files with a layered check.
|
Detect auto-generated files with a layered check.
|
||||||
Trust an explicit in-file marker first, for example `AUTO-GENERATED, DO NOT EDIT`.
|
Trust an explicit in-file marker first, for example `AUTO-GENERATED, DO NOT EDIT`.
|
||||||
@@ -69,6 +68,20 @@
|
|||||||
after applying an accepted change.
|
after applying an accepted change.
|
||||||
If the workflow is paused, say that plainly instead of using a dangling transition like "continuing" or "next".
|
If the workflow is paused, say that plainly instead of using a dangling transition like "continuing" or "next".
|
||||||
|
|
||||||
|
## Corrections
|
||||||
|
|
||||||
|
- When you discover that your belief about a project fact or convention was wrong, record the correction.
|
||||||
|
Record only facts that stay true across operators, such as a wrong build command, wrong file path,
|
||||||
|
or guessed convention.
|
||||||
|
Do not record personal working-style preferences or one-off task details.
|
||||||
|
- Record the correction in the nearest project `AGENTS.md`.
|
||||||
|
Use a dedicated `## Gotchas` section.
|
||||||
|
Create that section when the file lacks one.
|
||||||
|
- If no project `AGENTS.md` exists, ask before creating one.
|
||||||
|
Append the correction immediately without asking when a project `AGENTS.md` exists.
|
||||||
|
Briefly mention the edit in your response.
|
||||||
|
If an existing entry is wrong or stale, correct or remove it the same way.
|
||||||
|
|
||||||
## Gotchas
|
## Gotchas
|
||||||
|
|
||||||
- When materializing an external specification, standard, or project-maintained convention, use the
|
- When materializing an external specification, standard, or project-maintained convention, use the
|
||||||
@@ -88,12 +101,17 @@
|
|||||||
- Ariadne Rust tests are declared in Bazel `BUILD` files; do not add `[[test]]` entries to
|
- Ariadne Rust tests are declared in Bazel `BUILD` files; do not add `[[test]]` entries to
|
||||||
`src/ariadne/Cargo.toml` unless explicitly requested.
|
`src/ariadne/Cargo.toml` unless explicitly requested.
|
||||||
- In this monorepo, "dotfiles" refers to the dotfiles/NixOS configuration under `src/dotfiles/` and `configs/nixos/`.
|
- In this monorepo, "dotfiles" refers to the dotfiles/NixOS configuration under `src/dotfiles/` and `configs/nixos/`.
|
||||||
|
- The flake does not currently define `formatter.x86_64-linux`.
|
||||||
|
Do not use `nix fmt` as a validation command until a formatter is added.
|
||||||
- The `fix` app passes its current directory to Deadnix.
|
- The `fix` app passes its current directory to Deadnix.
|
||||||
Run `nix run .#fix` from the repository root so path exclusions match.
|
Run `nix run .#fix` from the repository root so path exclusions match.
|
||||||
|
- During the Nix refactor, address each issue that comes up by adding either a Natu rule or a new Nix style rule.
|
||||||
- Git pathspec searches started in a subdirectory default to that subtree.
|
- Git pathspec searches started in a subdirectory default to that subtree.
|
||||||
Use `git -C <repository-root>` or top-anchored pathspecs for repository-wide history searches.
|
Use `git -C <repository-root>` or top-anchored pathspecs for repository-wide history searches.
|
||||||
- A `**/*.nix` Git pathspec does not include Nix files directly in its starting directory.
|
- A `**/*.nix` Git pathspec does not include Nix files directly in its starting directory.
|
||||||
Include both `*.nix` and `**/*.nix`, or use a recursive `*.nix` pathspec, when linting every Nix file.
|
Include both `*.nix` and `**/*.nix`, or use a recursive `*.nix` pathspec, when linting every Nix file.
|
||||||
|
- Nix `let` bindings are recursive; their source order does not determine binding availability.
|
||||||
|
Treat Natu `let_order` as a readability rule, not an evaluation dependency.
|
||||||
- Ariadne extraction must preserve the game-root season prefix for archived source.
|
- Ariadne extraction must preserve the game-root season prefix for archived source.
|
||||||
Different game installs can contain compiled scripts with the same internal path.
|
Different game installs can contain compiled scripts with the same internal path.
|
||||||
- Clew owns Ren'Py extraction, preparation, and compiled-script AST export. Ariadne owns analysis over Clew-produced data.
|
- Clew owns Ren'Py extraction, preparation, and compiled-script AST export. Ariadne owns analysis over Clew-produced data.
|
||||||
@@ -104,13 +122,7 @@
|
|||||||
- OMP does not discover project skills from `.pi/skills`.
|
- OMP does not discover project skills from `.pi/skills`.
|
||||||
Put skills shared by Pi and OMP under `.agents/skills`, or configure OMP's `skills.customDirectories`.
|
Put skills shared by Pi and OMP under `.agents/skills`, or configure OMP's `skills.customDirectories`.
|
||||||
- Pi is vendored under `third_party/pi` as a linear vendor import.
|
- Pi is vendored under `third_party/pi` as a linear vendor import.
|
||||||
`pkgs.alexion.pi-coding-agent` packages that source and does not use Nixpkgs Pi.
|
|
||||||
Do not use `git subtree add` or `git subtree pull` for Pi PR branches.
|
Do not use `git subtree add` or `git subtree pull` for Pi PR branches.
|
||||||
Gitea rebases the synthetic upstream root commit without the `third_party/pi` prefix.
|
Gitea rebases the synthetic upstream root commit without the `third_party/pi` prefix.
|
||||||
Preserve `git-subtree-dir` and `git-subtree-split` trailers on Pi vendor commits.
|
Preserve `git-subtree-dir` and `git-subtree-split` trailers on Pi vendor commits.
|
||||||
- The default Pi model for NixOS user configuration is set in `configs/nixos/modules/shell/pi/default.nix`, not in vendored Pi source.
|
- The default Pi model for NixOS user configuration is set in `configs/nixos/modules/shell/pi/default.nix`, not in vendored Pi source.
|
||||||
- The `monorepo` and `status` libraries do not have `README.md` files.
|
|
||||||
- Check root and `src/skills/.pi/skills/` for enabled symlinks before removing a monorepo skill.
|
|
||||||
- Ariadne Rust source files live directly under `src/ariadne/`. The binary entry point is `ariadne_main.rs.
|
|
||||||
- Bazel build-style rules live under `src/skills/style/build/rules/`, not `src/skills/style/rules/`.
|
|
||||||
- Bazel passes arguments after `--` to the executed binary verbatim. A scaffold can define `//path` as a destination-path syntax.
|
|
||||||
|
|||||||
154
Cargo.lock
generated
154
Cargo.lock
generated
@@ -2,6 +2,15 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@@ -62,6 +71,12 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.6.7"
|
version = "4.6.7"
|
||||||
@@ -93,7 +108,7 @@ dependencies = [
|
|||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 3.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -108,6 +123,41 @@ version = "1.0.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "countme"
|
||||||
|
version = "3.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "googletest"
|
||||||
|
version = "0.14.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6b5e2f2b556b7b90297a5a35c8267dd43a537923d2b329beefdba2b4ec19d94"
|
||||||
|
dependencies = [
|
||||||
|
"googletest_macro",
|
||||||
|
"num-traits",
|
||||||
|
"regex",
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "googletest_macro"
|
||||||
|
version = "0.14.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2ae6abc96141edd26bf5aeec0f119c129c44de3ced09e5073711a02cb74725d0"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.14.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -140,6 +190,27 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
|||||||
name = "monorepo"
|
name = "monorepo"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "natu"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
"googletest",
|
||||||
|
"monorepo",
|
||||||
|
"rnix",
|
||||||
|
"rowan",
|
||||||
|
"status",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell_polyfill"
|
name = "once_cell_polyfill"
|
||||||
version = "1.70.2"
|
version = "1.70.2"
|
||||||
@@ -164,6 +235,68 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rnix"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c163bd17372eecdf10d351c34584b7de7c1a33be4e92a32f3fb3f5a7fe3f579b"
|
||||||
|
dependencies = [
|
||||||
|
"rowan",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rowan"
|
||||||
|
version = "0.16.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21"
|
||||||
|
dependencies = [
|
||||||
|
"countme",
|
||||||
|
"hashbrown",
|
||||||
|
"rustc-hash",
|
||||||
|
"text-size",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-hash"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.229"
|
version = "1.0.229"
|
||||||
@@ -191,7 +324,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 3.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -217,6 +350,17 @@ version = "0.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.119"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "3.0.6"
|
version = "3.0.6"
|
||||||
@@ -228,6 +372,12 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "text-size"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.26"
|
version = "1.0.26"
|
||||||
|
|||||||
@@ -4,12 +4,16 @@ members = [
|
|||||||
"src/machoke",
|
"src/machoke",
|
||||||
"src/monorepo",
|
"src/monorepo",
|
||||||
"src/status",
|
"src/status",
|
||||||
|
"src/natu",
|
||||||
]
|
]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
clap = "4"
|
clap = "4"
|
||||||
|
googletest = "0.14"
|
||||||
monorepo = { path = "src/monorepo" }
|
monorepo = { path = "src/monorepo" }
|
||||||
|
rnix = "=0.14.0"
|
||||||
|
rowan = "0.16"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
status = { path = "src/status" }
|
status = { path = "src/status" }
|
||||||
|
|||||||
67
configs/nixos/AGENTS.md
Normal file
67
configs/nixos/AGENTS.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# 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, records the imported closure as the target system profile,
|
||||||
|
and activates it. Do not rely on a target-local monorepo checkout for remote deployment.
|
||||||
|
- On Pikachu, `nix copy --to ssh://pikachu` accepted a small `nix store add-file` probe,
|
||||||
|
but rejected locally built system paths that lacked trusted signatures.
|
||||||
|
Use a sudo `nix-store --import` path for unsigned deploy closures.
|
||||||
|
- Public DNS entries are not managed through the NixOS configuration.
|
||||||
|
Do not describe NixOS route choices as public-DNS management.
|
||||||
|
- Do not hand-author `hardware-configuration.nix` for a host that has not been generated on that machine.
|
||||||
|
Generate it on the target system or live ISO, then stage it before evaluating the flake.
|
||||||
|
- Configure the Chaotic Nyx cache only on hosts that select `linuxPackages_cachyos` (currently Neogaia
|
||||||
|
and Zeus), not in the generic host module.
|
||||||
|
- Module-relative paths resolve from the module file. Update them when moving a module into a subdirectory.
|
||||||
|
- On Pikachu, `uptime` does not support `-s`. Use `who -b` to report the boot time.
|
||||||
51
configs/nixos/guests/actualbudget.nix
Normal file
51
configs/nixos/guests/actualbudget.nix
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.guests.actualbudget; # 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,14 +4,58 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.guests.copyparty;
|
cfg = config.guests.copyparty; # NOLINT(let_order)
|
||||||
|
|
||||||
|
volumeType = lib.types.submodule {
|
||||||
|
options = {
|
||||||
|
hostPath = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Host path bind-mounted into the Copyparty guest for this volume.";
|
||||||
|
};
|
||||||
|
|
||||||
|
path = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Guest path served by this Copyparty volume.";
|
||||||
|
};
|
||||||
|
|
||||||
|
access = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
|
||||||
|
default = { };
|
||||||
|
description = "Copyparty access map from permission string to account names.";
|
||||||
|
};
|
||||||
|
|
||||||
|
readOnly = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Serve this Copyparty volume read-only.";
|
||||||
|
};
|
||||||
|
|
||||||
|
flags = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
default = [ ];
|
||||||
|
description = "Copyparty volume flags.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.guests.copyparty = {
|
options.guests.copyparty = {
|
||||||
|
accountName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "alexion";
|
||||||
|
description = "Copyparty account name configured for password authentication.";
|
||||||
|
};
|
||||||
|
|
||||||
passwordSecret = lib.mkOption {
|
passwordSecret = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "services/copyparty/users/${config.user.name}/password";
|
default = "services/copyparty/users/alexion/password";
|
||||||
description = "SOPS secret name that contains the Copyparty account password.";
|
description = "SOPS secret name containing the Copyparty account password.";
|
||||||
|
};
|
||||||
|
|
||||||
|
stateDir = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "/var/lib/copyparty";
|
||||||
|
description = "Guest path where Copyparty stores service state.";
|
||||||
};
|
};
|
||||||
|
|
||||||
statePath = lib.mkOption {
|
statePath = lib.mkOption {
|
||||||
@@ -28,39 +72,7 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
volumes = lib.mkOption {
|
volumes = lib.mkOption {
|
||||||
type = lib.types.attrsOf (
|
type = lib.types.attrsOf volumeType;
|
||||||
lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
hostPath = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "Host path bind-mounted into the Copyparty guest for this volume.";
|
|
||||||
};
|
|
||||||
|
|
||||||
path = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "Guest path served by this Copyparty volume.";
|
|
||||||
};
|
|
||||||
|
|
||||||
access = lib.mkOption {
|
|
||||||
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
|
|
||||||
default = { };
|
|
||||||
description = "Copyparty access map from permission string to account names.";
|
|
||||||
};
|
|
||||||
|
|
||||||
readOnly = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = false;
|
|
||||||
description = "Serve this Copyparty volume read-only.";
|
|
||||||
};
|
|
||||||
|
|
||||||
flags = lib.mkOption {
|
|
||||||
type = lib.types.attrs;
|
|
||||||
default = { };
|
|
||||||
description = "Copyparty volume flags.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
default = { };
|
default = { };
|
||||||
description = "Copyparty volumes keyed by their URL path.";
|
description = "Copyparty volumes keyed by their URL path.";
|
||||||
};
|
};
|
||||||
@@ -90,26 +102,24 @@ in
|
|||||||
guests.copyparty = {
|
guests.copyparty = {
|
||||||
interior.modules.services.copyparty = {
|
interior.modules.services.copyparty = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
inherit (cfg) accountName stateDir;
|
||||||
passwordFile = "/run/secrets/${cfg.passwordSecret}";
|
passwordFile = "/run/secrets/${cfg.passwordSecret}";
|
||||||
reverseProxy = {
|
reverseProxy = {
|
||||||
enable = cfg.reverseProxy.enable;
|
enable = cfg.reverseProxy.enable;
|
||||||
inherit (cfg.reverseProxy) trustedSources;
|
inherit (cfg.reverseProxy) trustedSources;
|
||||||
};
|
};
|
||||||
volumes = lib.mapAttrs (
|
volumes = lib.mapAttrs (_urlPath: volume: {
|
||||||
_urlPath: volume:
|
inherit (volume)
|
||||||
{
|
path
|
||||||
inherit (volume)
|
access
|
||||||
path
|
flags
|
||||||
readOnly
|
readOnly
|
||||||
;
|
;
|
||||||
}
|
}) cfg.volumes;
|
||||||
// lib.optionalAttrs (volume.access != { }) { inherit (volume) access; }
|
|
||||||
// lib.optionalAttrs (volume.flags != { }) { inherit (volume) flags; }
|
|
||||||
) cfg.volumes;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
mounts = {
|
mounts = {
|
||||||
"/var/lib/copyparty" = {
|
${cfg.stateDir} = {
|
||||||
directory = cfg.statePath;
|
directory = cfg.statePath;
|
||||||
owner = config.registry."stable-uid".${config.networking.hostName}.copyparty;
|
owner = config.registry."stable-uid".${config.networking.hostName}.copyparty;
|
||||||
group = "storage";
|
group = "storage";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.guests.homepage;
|
cfg = config.guests.homepage; # NOLINT(let_order)
|
||||||
environmentFile = "/run/secrets/homepage.env";
|
environmentFile = "/run/secrets/homepage.env";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.guests.palworld;
|
cfg = config.guests.palworld; # NOLINT(let_order)
|
||||||
steamDir = "/data/steamcmd";
|
steamDir = "/data/steamcmd";
|
||||||
installDir = "/data/server";
|
installDir = "/data/server";
|
||||||
stateDir = "/data/state";
|
stateDir = "/data/state";
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ 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;
|
||||||
|
|||||||
@@ -1,181 +0,0 @@
|
|||||||
{
|
|
||||||
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;
|
cfg = config.guests.valheim; # NOLINT(let_order)
|
||||||
steamDir = "/data/steamcmd";
|
steamDir = "/data/steamcmd";
|
||||||
installDir = "/data/server";
|
installDir = "/data/server";
|
||||||
stateDir = "/data/state";
|
stateDir = "/data/state";
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
inputs,
|
inputs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# neogaia — Dell XPS 13 9380 laptop.
|
# neogaia — Dell XPS 13 9380 laptop.
|
||||||
@@ -15,9 +14,8 @@
|
|||||||
|
|
||||||
modules.host.cachyos.enable = true;
|
modules.host.cachyos.enable = true;
|
||||||
|
|
||||||
services.xserver.xkb.options = "caps:swapescape";
|
# So wifi can be joined from the console.
|
||||||
|
networking.networkmanager.enable = true;
|
||||||
modules.network.enable = true;
|
|
||||||
|
|
||||||
registry.address.neogaia.self = "10.23.50.146";
|
registry.address.neogaia.self = "10.23.50.146";
|
||||||
|
|
||||||
@@ -29,9 +27,15 @@
|
|||||||
peer = {
|
peer = {
|
||||||
publicKey = "sGEkSD+gEdIMXi4PBN3AhwHGc4tWMtcjOIey3s7yb3I=";
|
publicKey = "sGEkSD+gEdIMXi4PBN3AhwHGc4tWMtcjOIey3s7yb3I=";
|
||||||
endpoint = "100.0.90.192:51820";
|
endpoint = "100.0.90.192:51820";
|
||||||
|
allowedIPs = [ "10.23.0.0/16" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
registry.shell-shortcut.neogaia = {
|
||||||
|
vpn-on = "sudo systemctl start wireguard-wg-home.target";
|
||||||
|
vpn-off = "sudo systemctl stop 'wireguard-wg-home*.service' wireguard-wg-home.target";
|
||||||
|
};
|
||||||
|
|
||||||
modules.monorepo.enable = true;
|
modules.monorepo.enable = true;
|
||||||
modules.shell.herdr.enable = true;
|
modules.shell.herdr.enable = true;
|
||||||
modules.shell.gitea-axi.enable = true;
|
modules.shell.gitea-axi.enable = true;
|
||||||
@@ -42,23 +46,4 @@
|
|||||||
modules.desktop.apps.obsidian.enable = true;
|
modules.desktop.apps.obsidian.enable = true;
|
||||||
modules.desktop.apps.steam.enable = true;
|
modules.desktop.apps.steam.enable = true;
|
||||||
|
|
||||||
registry.waybar.neogaia.battery = {
|
|
||||||
order = 70;
|
|
||||||
settings = {
|
|
||||||
format = "{icon} {capacity}%";
|
|
||||||
format-charging = "${util.glyph "f0e7"} {capacity}%";
|
|
||||||
format-icons = [
|
|
||||||
(util.glyph "f244")
|
|
||||||
(util.glyph "f243")
|
|
||||||
(util.glyph "f242")
|
|
||||||
(util.glyph "f241")
|
|
||||||
(util.glyph "f240")
|
|
||||||
];
|
|
||||||
states = {
|
|
||||||
warning = 20;
|
|
||||||
critical = 10;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,22 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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";
|
||||||
|
protocol = "https";
|
||||||
|
port = 5006;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
guests.navidrome = {
|
guests.navidrome = {
|
||||||
enable = true;
|
enable = true;
|
||||||
vlan = 20;
|
vlan = 20;
|
||||||
@@ -88,26 +104,6 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
guests.romm = {
|
|
||||||
enable = true;
|
|
||||||
vlan = 20;
|
|
||||||
address = "10.23.20.112";
|
|
||||||
dataPath = "/pikachu/data/romm";
|
|
||||||
postgresqlPath = "/pikachu/data/romm/postgresql";
|
|
||||||
redisPath = "/pikachu/data/romm/redis";
|
|
||||||
romsPath = "/pikachu/media/games/emulation/roms";
|
|
||||||
biosPath = "/pikachu/media/games/emulation/bios";
|
|
||||||
usersPath = "/pikachu/media/games/emulation/users";
|
|
||||||
reverseProxy = {
|
|
||||||
enable = true;
|
|
||||||
route = {
|
|
||||||
domain = "games.alexion.dev";
|
|
||||||
protocol = "https";
|
|
||||||
port = 80;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
guests.reverse-proxy = {
|
guests.reverse-proxy = {
|
||||||
enable = true;
|
enable = true;
|
||||||
vlan = 20;
|
vlan = 20;
|
||||||
@@ -149,21 +145,35 @@
|
|||||||
"/" = {
|
"/" = {
|
||||||
hostPath = "/pikachu/data/copyparty/root";
|
hostPath = "/pikachu/data/copyparty/root";
|
||||||
path = "/srv";
|
path = "/srv";
|
||||||
|
access.r = [ "alexion" ];
|
||||||
readOnly = true;
|
readOnly = true;
|
||||||
};
|
};
|
||||||
"/inbox" = {
|
"/inbox" = {
|
||||||
hostPath = "/pikachu/__inbox";
|
hostPath = "/pikachu/__inbox";
|
||||||
path = "/pikachu/__inbox";
|
path = "/pikachu/__inbox";
|
||||||
|
access.A = [ "alexion" ];
|
||||||
|
flags = [
|
||||||
|
"d2d"
|
||||||
|
"d2t"
|
||||||
|
"dthumb"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
"/media" = {
|
"/media" = {
|
||||||
hostPath = "/pikachu/media";
|
hostPath = "/pikachu/media";
|
||||||
path = "/pikachu/media";
|
path = "/pikachu/media";
|
||||||
|
access.A = [ "alexion" ];
|
||||||
|
flags = [
|
||||||
|
"d2d"
|
||||||
|
"d2t"
|
||||||
|
"dthumb"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
"/opds" = {
|
"/opds" = {
|
||||||
hostPath = "/pikachu/data/opds";
|
hostPath = "/pikachu/data/opds";
|
||||||
path = "/pikachu/data/opds";
|
path = "/pikachu/data/opds";
|
||||||
|
access.r = [ "alexion" ];
|
||||||
readOnly = true;
|
readOnly = true;
|
||||||
flags.opds = true;
|
flags = [ "opds" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
enable32Bit = true;
|
enable32Bit = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
modules.network.enable = true;
|
networking.networkmanager.enable = true;
|
||||||
|
|
||||||
services.logind.settings.Login = {
|
services.logind.settings.Login = {
|
||||||
HandlePowerKey = "ignore";
|
HandlePowerKey = "ignore";
|
||||||
@@ -49,19 +49,8 @@
|
|||||||
modules.desktop.apps.vencord.enable = true;
|
modules.desktop.apps.vencord.enable = true;
|
||||||
|
|
||||||
home-manager.users.${config.user.name}.wayland.windowManager.hyprland.settings.monitor = [
|
home-manager.users.${config.user.name}.wayland.windowManager.hyprland.settings.monitor = [
|
||||||
{
|
"HDMI-A-1,1920x1080@120,0x0,1,transform,3"
|
||||||
output = "HDMI-A-1";
|
"DP-3,2560x1440@239.96,1080x0,1"
|
||||||
mode = "1920x1080@120";
|
|
||||||
position = "-1080x0";
|
|
||||||
scale = 1;
|
|
||||||
transform = 3;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
output = "DP-3";
|
|
||||||
mode = "2560x1440@239.96";
|
|
||||||
position = "0x0";
|
|
||||||
scale = 1;
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
18
configs/nixos/modules/desktop/apps/alacritty.nix
Normal file
18
configs/nixos/modules/desktop/apps/alacritty.nix
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
# Alacritty as the desktop terminal.
|
||||||
|
let
|
||||||
|
user = config.user.name;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.modules.desktop.apps.alacritty.enable =
|
||||||
|
lib.mkEnableOption "Alacritty as the desktop terminal";
|
||||||
|
|
||||||
|
config = lib.mkIf config.modules.desktop.apps.alacritty.enable {
|
||||||
|
home-manager.users.${user}.home.packages = [ pkgs.alacritty ];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,12 +3,13 @@
|
|||||||
inputs,
|
inputs,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Configures Firefox as the desktop browser.
|
# Firefox as the desktop browser: stock mainline, hardened and de-monetized by policy.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.apps.firefox;
|
cfg = config.modules.desktop.apps.firefox; # NOLINT(let_order)
|
||||||
|
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 = {
|
||||||
@@ -18,25 +19,19 @@ in
|
|||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "1.0";
|
default = "1.0";
|
||||||
description = ''
|
description = ''
|
||||||
Scales Firefox UI and page content.
|
Firefox UI and page scale, passed to `layout.css.devPixelsPerPx`.
|
||||||
|
Override it on hosts that need a larger browser scale, such as small
|
||||||
Sets `layout.css.devPixelsPerPx`.
|
high-DPI laptop panels.
|
||||||
Use a larger value for high-DPI displays.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
registry.window-icon.${config.networking.hostName}.firefox = {
|
home-manager.users.${user} = hm: {
|
||||||
class = ".*[Ff]irefox.*";
|
|
||||||
icon = util.glyph "f269";
|
|
||||||
};
|
|
||||||
|
|
||||||
home-manager.users.${config.user.name} = hm: {
|
|
||||||
programs.firefox = {
|
programs.firefox = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
# Applies policies that users cannot change in Firefox.
|
# Enterprise policies: enforced and unchangeable from the browser UI.
|
||||||
policies = {
|
policies = {
|
||||||
DisableTelemetry = true;
|
DisableTelemetry = true;
|
||||||
DisableFirefoxStudies = true;
|
DisableFirefoxStudies = true;
|
||||||
@@ -45,7 +40,7 @@ in
|
|||||||
DontCheckDefaultBrowser = true;
|
DontCheckDefaultBrowser = true;
|
||||||
DisableFirefoxAccounts = true;
|
DisableFirefoxAccounts = true;
|
||||||
|
|
||||||
# Removes sponsored content from the new-tab page.
|
# Strip the monetized surfaces from the new-tab page.
|
||||||
FirefoxHome = {
|
FirefoxHome = {
|
||||||
SponsoredTopSites = false;
|
SponsoredTopSites = false;
|
||||||
SponsoredPocket = false;
|
SponsoredPocket = false;
|
||||||
@@ -54,21 +49,23 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
profiles.default = {
|
profiles.default = {
|
||||||
|
isDefault = true;
|
||||||
|
|
||||||
extensions = {
|
extensions = {
|
||||||
packages = with inputs.firefox-addons.packages.${pkgs.stdenv.hostPlatform.system}; [
|
packages = with firefoxAddons; [
|
||||||
ublock-origin
|
ublock-origin
|
||||||
proton-pass
|
proton-pass
|
||||||
sponsorblock
|
sponsorblock
|
||||||
];
|
];
|
||||||
|
|
||||||
# Home Manager overwrites Nord theme changes made in Firefox.
|
# The Nord chrome theme is a declared extension setting, so home-manager
|
||||||
|
# owns the extension-settings store, overwriting runtime changes to it.
|
||||||
force = true;
|
force = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
# Stylix uses a near-white selection background.
|
# Stylix's Nord mapping paints the selected address-bar result a
|
||||||
# Use the Nord selection color because its text stays readable.
|
# near-white grey, leaving its light text unreadable. Darken that one
|
||||||
# Home Manager writes extension settings through Firefox's legacy storage backend.
|
# highlight to the Nord selection grey, from the same scheme.
|
||||||
# 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;
|
||||||
@@ -80,26 +77,32 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
|
# Scale the UI and page by a fixed factor.
|
||||||
|
# Left at auto (-1), Firefox reads the panel's 1.5x and inflates its
|
||||||
|
# whole chrome while point-sized apps stay put.
|
||||||
|
# A shade under that brings it into line without dropping to true
|
||||||
|
# 1:1, which reads too small at this DPI.
|
||||||
"layout.css.devPixelsPerPx" = cfg.uiScale;
|
"layout.css.devPixelsPerPx" = cfg.uiScale;
|
||||||
|
|
||||||
# Enables Firefox Color after Firefox sideloads the add-on.
|
# Auto-enable the sideloaded Firefox Color add-on carrying the Nord
|
||||||
|
# chrome theme, which Firefox otherwise leaves disabled.
|
||||||
"extensions.autoDisableScopes" = 0;
|
"extensions.autoDisableScopes" = 0;
|
||||||
|
|
||||||
# Removes sponsored content outside Firefox policies.
|
# Sponsored surfaces the policies above do not reach.
|
||||||
"browser.urlbar.suggest.quicksuggest.sponsored" = false;
|
"browser.urlbar.suggest.quicksuggest.sponsored" = false;
|
||||||
"browser.newtabpage.activity-stream.showSponsored" = false;
|
"browser.newtabpage.activity-stream.showSponsored" = false;
|
||||||
"browser.newtabpage.activity-stream.showSponsoredTopSites" = false;
|
"browser.newtabpage.activity-stream.showSponsoredTopSites" = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
search = {
|
search = {
|
||||||
# Home Manager replaces the engine list when this setting is enabled.
|
# Declaring search overwrites Firefox's own engine list wholesale,
|
||||||
# Engines added in Firefox do not survive a rebuild.
|
# so engines added later in the UI do not survive a rebuild.
|
||||||
force = true;
|
force = true;
|
||||||
default = "ddg";
|
default = "ddg";
|
||||||
|
|
||||||
# Hides commercial search engines.
|
# The general-purpose commercial engines, hidden to leave a lean
|
||||||
# Bangs still reach these engines.
|
# DuckDuckGo-and-Wikipedia list. They stay reachable through bangs.
|
||||||
# Entries with only `metaData` configure Firefox built-in engines.
|
# An engine carrying only metaData is treated as a builtin.
|
||||||
engines = {
|
engines = {
|
||||||
google.metaData.hidden = true;
|
google.metaData.hidden = true;
|
||||||
bing.metaData.hidden = true;
|
bing.metaData.hidden = true;
|
||||||
@@ -111,7 +114,8 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# Applies the Nord theme through Stylix's Firefox Color add-on.
|
# Nord chrome for the one profile, applied through the Stylix-managed
|
||||||
|
# Firefox Color add-on that colorTheme enables.
|
||||||
stylix.targets.firefox = {
|
stylix.targets.firefox = {
|
||||||
enable = true;
|
enable = true;
|
||||||
profileNames = [ "default" ];
|
profileNames = [ "default" ];
|
||||||
@@ -129,8 +133,8 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# Firefox creates `profiles.ini` on its first launch.
|
# Firefox writes profiles.ini itself on first launch, so home-manager is
|
||||||
# Force replacement during Home Manager activation.
|
# told to own the file rather than fail activation refusing to clobber it.
|
||||||
home.file."${hm.config.programs.firefox.configPath}/profiles.ini".force = true;
|
home.file."${hm.config.programs.firefox.configPath}/profiles.ini".force = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Ghostty as the desktop terminal.
|
# Ghostty as the desktop terminal.
|
||||||
@@ -13,19 +12,6 @@ in
|
|||||||
options.modules.desktop.apps.ghostty.enable = lib.mkEnableOption "Ghostty as the desktop terminal";
|
options.modules.desktop.apps.ghostty.enable = lib.mkEnableOption "Ghostty as the desktop terminal";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.apps.ghostty.enable {
|
config = lib.mkIf 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,7 +2,6 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Obsidian as the desktop note-taking vault.
|
# Obsidian as the desktop note-taking vault.
|
||||||
@@ -15,10 +14,5 @@ in
|
|||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.apps.obsidian.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";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Signal desktop messenger.
|
# Signal desktop messenger.
|
||||||
@@ -14,10 +13,5 @@ in
|
|||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.apps.signal.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,7 +2,6 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Steam game launcher and runtime integration.
|
# Steam game launcher and runtime integration.
|
||||||
@@ -24,10 +23,5 @@
|
|||||||
environment.systemPackages = [
|
environment.systemPackages = [
|
||||||
pkgs.gamescope
|
pkgs.gamescope
|
||||||
];
|
];
|
||||||
|
|
||||||
registry.window-icon.${config.networking.hostName}.steam = {
|
|
||||||
class = "[Ss]team";
|
|
||||||
icon = util.glyph "f1b6";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Vesktop, a Discord client with Vencord built in.
|
# Vesktop, a Discord client with Vencord built in.
|
||||||
@@ -15,10 +14,5 @@ in
|
|||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.apps.vencord.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";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,5 @@
|
|||||||
modules.desktop.utilities.screenshot.enable = lib.mkDefault true;
|
modules.desktop.utilities.screenshot.enable = lib.mkDefault true;
|
||||||
modules.desktop.utilities.theming.enable = lib.mkDefault true;
|
modules.desktop.utilities.theming.enable = lib.mkDefault true;
|
||||||
modules.desktop.utilities.userdirs.enable = lib.mkDefault true;
|
modules.desktop.utilities.userdirs.enable = lib.mkDefault true;
|
||||||
registry.env.${config.networking.hostName}.NIXOS_OZONE_WL = "1";
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,42 @@
|
|||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
util,
|
pkgs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# 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.enable = lib.mkEnableOption "the Hyprland compositor";
|
options.modules.desktop.session.hyprland = {
|
||||||
|
enable = lib.mkEnableOption "the Hyprland compositor";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.session.hyprland.enable {
|
blur = lib.mkEnableOption ''
|
||||||
|
window blur. Off by default as the single biggest battery cost on a
|
||||||
|
laptop, left on for a host with the headroom to spend it'';
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
# This program integration owns the session, portals, and polkit, launched
|
# This program integration owns the session, portals, and polkit, launched
|
||||||
# through the universal Wayland session manager.
|
# through the universal Wayland session manager.
|
||||||
programs.hyprland = {
|
programs.hyprland = {
|
||||||
@@ -17,7 +44,7 @@
|
|||||||
withUWSM = true;
|
withUWSM = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
home-manager.users.${config.user.name}.wayland.windowManager.hyprland = {
|
home-manager.users.${user}.wayland.windowManager.hyprland = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
# One package drives the whole session, so there is never a version split.
|
# One package drives the whole session, so there is never a version split.
|
||||||
@@ -29,217 +56,135 @@
|
|||||||
# uwsm owns the systemd graphical-session targets.
|
# uwsm owns the systemd graphical-session targets.
|
||||||
systemd.enable = false;
|
systemd.enable = false;
|
||||||
|
|
||||||
# Write the native Lua Hyprland config. Home Manager renders the Nix
|
# Write the native hyprlang hyprland.conf, whose variable and bind syntax
|
||||||
# settings below as hl.* calls.
|
# the settings below are expressed in.
|
||||||
configType = "lua";
|
configType = "hyprlang";
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
config = {
|
"$mod" = "SUPER";
|
||||||
input = {
|
"$terminal" = "ghostty";
|
||||||
kb_layout = config.services.xserver.xkb.layout;
|
|
||||||
kb_options = config.services.xserver.xkb.options;
|
# Session variables handed to the compositor directly.
|
||||||
repeat_delay = 250;
|
# UWSM launches the session without the shell profile that would carry
|
||||||
repeat_rate = 45;
|
# them, so a variable the compositor or its children must see is set
|
||||||
accel_profile = "flat";
|
# here rather than through home-manager's sessionVariables.
|
||||||
touchpad = {
|
env = [
|
||||||
natural_scroll = true;
|
# Chromium and Electron apps read this to select native Wayland;
|
||||||
tap_to_click = true;
|
# nixpkgs wrappers (Obsidian's included) gate their Wayland flags on
|
||||||
disable_while_typing = true;
|
# it, so without it they run under XWayland and blur at this DPI.
|
||||||
};
|
"NIXOS_OZONE_WL,1"
|
||||||
|
]
|
||||||
|
# Bibata ships XCursor only.
|
||||||
|
# The hyprcursor variables name the same theme, which Hyprland
|
||||||
|
# resolves through its XCursor fallback.
|
||||||
|
++ lib.optionals (cursor != null) [
|
||||||
|
"XCURSOR_THEME,${cursor.name}"
|
||||||
|
"XCURSOR_SIZE,${toString cursor.size}"
|
||||||
|
"HYPRCURSOR_THEME,${cursor.name}"
|
||||||
|
"HYPRCURSOR_SIZE,${toString cursor.size}"
|
||||||
|
];
|
||||||
|
|
||||||
|
input = {
|
||||||
|
kb_layout = "us";
|
||||||
|
# Caps is a second Escape.
|
||||||
|
# Shift+Caps still toggles a real CapsLock.
|
||||||
|
kb_options = "caps:escape_shifted_capslock";
|
||||||
|
# Snappy key repeat.
|
||||||
|
repeat_delay = 250;
|
||||||
|
repeat_rate = 45;
|
||||||
|
accel_profile = "flat";
|
||||||
|
touchpad = {
|
||||||
|
natural_scroll = true;
|
||||||
|
tap-to-click = true;
|
||||||
|
disable_while_typing = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
general = {
|
|
||||||
gaps_in = 4;
|
|
||||||
gaps_out = 8;
|
|
||||||
border_size = 2;
|
|
||||||
layout = "dwindle";
|
|
||||||
};
|
|
||||||
|
|
||||||
decoration = {
|
|
||||||
rounding = 6;
|
|
||||||
blur.enabled = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
xwayland.force_zero_scaling = true;
|
|
||||||
|
|
||||||
dwindle.preserve_split = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
env = lib.mapAttrsToList (name: value: {
|
general = {
|
||||||
_args = [
|
gaps_in = 4;
|
||||||
name
|
gaps_out = 8;
|
||||||
value
|
border_size = 2;
|
||||||
|
layout = "dwindle";
|
||||||
|
};
|
||||||
|
|
||||||
|
decoration = {
|
||||||
|
rounding = 6;
|
||||||
|
blur.enabled = cfg.blur;
|
||||||
|
};
|
||||||
|
|
||||||
|
# XWayland clients render at the panel's native resolution instead of
|
||||||
|
# being raster-scaled by the compositor at the fractional monitor scale.
|
||||||
|
xwayland.force_zero_scaling = true;
|
||||||
|
|
||||||
|
animations = {
|
||||||
|
enabled = true;
|
||||||
|
bezier = [ "ease, 0.25, 0.1, 0.25, 1.0" ];
|
||||||
|
# Durations are in centiseconds.
|
||||||
|
# Short values keep the motion subtle.
|
||||||
|
animation = [
|
||||||
|
"windows, 1, 3, ease"
|
||||||
|
"fade, 1, 3, ease"
|
||||||
|
# Layer surfaces like the launcher fade in a touch quicker than windows.
|
||||||
|
"layersIn, 1, 2, ease"
|
||||||
|
"fadeLayersIn, 1, 2, ease"
|
||||||
|
"workspaces, 1, 3, ease"
|
||||||
|
"border, 1, 3, ease"
|
||||||
];
|
];
|
||||||
}) (lib.attrByPath [ config.networking.hostName ] { } config.registry.env);
|
|
||||||
|
|
||||||
bind = lib.mapAttrsToList (_name: keybind: {
|
|
||||||
_args = [
|
|
||||||
keybind.keys
|
|
||||||
(lib.generators.mkLuaInline (
|
|
||||||
{
|
|
||||||
exec = "hl.dsp.exec_cmd(${lib.generators.toLua { } keybind.action.command})";
|
|
||||||
focus = "hl.dsp.focus(${
|
|
||||||
lib.generators.toLua { } { inherit (keybind.action) direction workspace; }
|
|
||||||
})";
|
|
||||||
"move-window" = "hl.dsp.window.move(${
|
|
||||||
lib.generators.toLua { } { inherit (keybind.action) direction workspace; }
|
|
||||||
})";
|
|
||||||
"resize-window" = "hl.dsp.window.resize(${
|
|
||||||
lib.generators.toLua { } (
|
|
||||||
{
|
|
||||||
down = {
|
|
||||||
x = 0;
|
|
||||||
y = 10;
|
|
||||||
relative = true;
|
|
||||||
};
|
|
||||||
left = {
|
|
||||||
x = -10;
|
|
||||||
y = 0;
|
|
||||||
relative = true;
|
|
||||||
};
|
|
||||||
right = {
|
|
||||||
x = 10;
|
|
||||||
y = 0;
|
|
||||||
relative = true;
|
|
||||||
};
|
|
||||||
up = {
|
|
||||||
x = 0;
|
|
||||||
y = -10;
|
|
||||||
relative = true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
.${keybind.action.direction}
|
|
||||||
)
|
|
||||||
})";
|
|
||||||
"toggle-floating" = ''hl.dsp.window.float({ action = "toggle" })'';
|
|
||||||
"toggle-fullscreen" = "hl.dsp.window.fullscreen()";
|
|
||||||
"close-window" = "hl.dsp.window.close()";
|
|
||||||
}
|
|
||||||
.${keybind.action.kind}
|
|
||||||
))
|
|
||||||
]
|
|
||||||
++ lib.optional keybind.repeating {
|
|
||||||
repeating = true;
|
|
||||||
}
|
|
||||||
++ lib.optional keybind.locked {
|
|
||||||
locked = true;
|
|
||||||
};
|
|
||||||
}) (lib.attrByPath [ config.networking.hostName ] { } config.registry.keybind);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
registry.keybind.${config.networking.hostName} = lib.mkMerge [
|
|
||||||
# Generate numeric workspace keybinds.
|
|
||||||
(lib.listToAttrs (
|
|
||||||
lib.concatMap (workspace: [
|
|
||||||
{
|
|
||||||
name = "hyprland-workspace-${toString workspace}";
|
|
||||||
value = {
|
|
||||||
keys = "SUPER + ${toString workspace}";
|
|
||||||
action = {
|
|
||||||
kind = "focus";
|
|
||||||
inherit workspace;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
{
|
|
||||||
name = "hyprland-move-to-workspace-${toString workspace}";
|
|
||||||
value = {
|
|
||||||
keys = "SUPER + SHIFT + ${toString workspace}";
|
|
||||||
action = {
|
|
||||||
kind = "move-window";
|
|
||||||
inherit workspace;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
]) (lib.range 1 9)
|
|
||||||
))
|
|
||||||
|
|
||||||
# Generate directional keybinds.
|
|
||||||
(lib.listToAttrs (
|
|
||||||
lib.concatMap
|
|
||||||
(
|
|
||||||
{ direction, key }:
|
|
||||||
[
|
|
||||||
{
|
|
||||||
name = "hyprland-focus-${direction}";
|
|
||||||
value = {
|
|
||||||
keys = "SUPER + ${key}";
|
|
||||||
action = {
|
|
||||||
kind = "focus";
|
|
||||||
inherit direction;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
{
|
|
||||||
name = "hyprland-move-${direction}";
|
|
||||||
value = {
|
|
||||||
keys = "SUPER + SHIFT + ${key}";
|
|
||||||
action = {
|
|
||||||
kind = "move-window";
|
|
||||||
inherit direction;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
{
|
|
||||||
name = "hyprland-resize-${direction}";
|
|
||||||
value = {
|
|
||||||
keys = "SUPER + ALT + ${key}";
|
|
||||||
repeating = true;
|
|
||||||
action = {
|
|
||||||
kind = "resize-window";
|
|
||||||
inherit direction;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
[
|
|
||||||
{
|
|
||||||
key = "H";
|
|
||||||
direction = "left";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
key = "J";
|
|
||||||
direction = "down";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
key = "K";
|
|
||||||
direction = "up";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
key = "L";
|
|
||||||
direction = "right";
|
|
||||||
}
|
|
||||||
]
|
|
||||||
))
|
|
||||||
{
|
|
||||||
hyprland-toggle-floating = {
|
|
||||||
keys = "SUPER + Space";
|
|
||||||
action.kind = "toggle-floating";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
hyprland-toggle-fullscreen = {
|
dwindle = {
|
||||||
keys = "SUPER + F";
|
preserve_split = true;
|
||||||
action.kind = "toggle-fullscreen";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
hyprland-close-window = {
|
bind = [
|
||||||
keys = "SUPER + SHIFT + Q";
|
"$mod, Return, exec, $terminal"
|
||||||
action.kind = "close-window";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
registry.waybar.${config.networking.hostName}."hyprland/workspaces" = {
|
# Move focus.
|
||||||
position = "left";
|
"$mod, H, movefocus, l"
|
||||||
order = 10;
|
"$mod, J, movefocus, d"
|
||||||
settings = {
|
"$mod, K, movefocus, u"
|
||||||
format = "{id} {windows}";
|
"$mod, L, movefocus, r"
|
||||||
format-window-separator = " ";
|
|
||||||
window-rewrite-default = util.glyph "f2d0";
|
# Move the window within the layout.
|
||||||
window-rewrite = lib.mapAttrs' (
|
"$mod SHIFT, H, movewindow, l"
|
||||||
_name: windowIcon: lib.nameValuePair "class<${windowIcon.class}>" windowIcon.icon
|
"$mod SHIFT, J, movewindow, d"
|
||||||
) (lib.attrByPath [ config.networking.hostName ] { } config.registry.window-icon);
|
"$mod SHIFT, K, movewindow, u"
|
||||||
|
"$mod SHIFT, L, movewindow, r"
|
||||||
|
|
||||||
|
# Resize the active window.
|
||||||
|
"$mod ALT, H, resizeactive, -40 0"
|
||||||
|
"$mod ALT, J, resizeactive, 0 40"
|
||||||
|
"$mod ALT, K, resizeactive, 0 -40"
|
||||||
|
"$mod ALT, L, resizeactive, 40 0"
|
||||||
|
|
||||||
|
"$mod, Space, togglefloating,"
|
||||||
|
"$mod, F, fullscreen,"
|
||||||
|
# togglesplit is a dwindle layout message, reached through layoutmsg.
|
||||||
|
"$mod, T, layoutmsg, togglesplit"
|
||||||
|
"$mod SHIFT, T, exec, ${toggleLayout}"
|
||||||
|
"$mod SHIFT, Q, killactive,"
|
||||||
|
"$mod CTRL, Q, forcekillactive,"
|
||||||
|
]
|
||||||
|
++ workspaceBinds;
|
||||||
|
|
||||||
|
# Volume and brightness keys repeat while held, each raising a popup
|
||||||
|
# through the OSD client.
|
||||||
|
# Volume is capped at 100 percent.
|
||||||
|
# The client floors brightness so a full hold cannot black the screen out.
|
||||||
|
binde = [
|
||||||
|
", XF86AudioRaiseVolume, exec, ${pkgs.swayosd}/bin/swayosd-client --output-volume raise --max-volume 100"
|
||||||
|
", XF86AudioLowerVolume, exec, ${pkgs.swayosd}/bin/swayosd-client --output-volume lower"
|
||||||
|
", XF86MonBrightnessUp, exec, ${pkgs.swayosd}/bin/swayosd-client --brightness raise"
|
||||||
|
", XF86MonBrightnessDown, exec, ${pkgs.swayosd}/bin/swayosd-client --brightness lower"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Mute and media transport still fire while the session is locked.
|
||||||
|
bindl = [
|
||||||
|
", XF86AudioMute, exec, ${pkgs.swayosd}/bin/swayosd-client --output-volume mute-toggle"
|
||||||
|
", XF86AudioPlay, exec, ${pkgs.playerctl}/bin/playerctl play-pause"
|
||||||
|
", XF86AudioNext, exec, ${pkgs.playerctl}/bin/playerctl next"
|
||||||
|
", XF86AudioPrev, exec, ${pkgs.playerctl}/bin/playerctl previous"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,34 +5,63 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# hypridle: idle-triggered locking and display power management.
|
# hypridle: idle-triggered locking and display power management.
|
||||||
{
|
let
|
||||||
options.modules.desktop.session.hyprland.hypridle.enable =
|
cfg = config.modules.desktop.session.hyprland.hypridle; # NOLINT(let_order)
|
||||||
lib.mkEnableOption "hypridle idle management";
|
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.session.hyprland.hypridle.enable {
|
# 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 = {
|
||||||
|
enable = lib.mkEnableOption "hypridle idle management";
|
||||||
|
|
||||||
|
lockTimeout = lib.mkOption {
|
||||||
|
type = lib.types.ints.positive;
|
||||||
|
default = 300;
|
||||||
|
description = "Seconds of inactivity before the screen locks.";
|
||||||
|
};
|
||||||
|
|
||||||
|
screenOffTimeout = lib.mkOption {
|
||||||
|
type = lib.types.ints.positive;
|
||||||
|
default = 360;
|
||||||
|
description = "Seconds of inactivity before the displays are powered off.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
# Closing the lid suspends, and every suspend locks first through the
|
# Closing the lid suspends, and every suspend locks first through the
|
||||||
# before_sleep_cmd below, so the lid always lands at a locked screen.
|
# before_sleep_cmd below, so the lid always lands at a locked screen.
|
||||||
services.logind.settings.Login.HandleLidSwitch = "suspend";
|
services.logind.settings.Login.HandleLidSwitch = "suspend";
|
||||||
|
|
||||||
home-manager.users.${config.user.name}.services.hypridle = {
|
home-manager.users.${user}.services.hypridle = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
general = {
|
general = {
|
||||||
# The guard drops the call when a locker is already up, so no idle
|
lock_cmd = lockCmd;
|
||||||
# trigger stacks a second hyprlock over the first.
|
|
||||||
lock_cmd = "${lib.getExe' pkgs.procps "pidof"} hyprlock || ${lib.getExe pkgs.hyprlock}";
|
|
||||||
before_sleep_cmd = "loginctl lock-session";
|
before_sleep_cmd = "loginctl lock-session";
|
||||||
# Waking pokes the compositor in case firmware leaves displays dark.
|
# Waking restores the displays the screen-off listener may have cut.
|
||||||
after_sleep_cmd = "${lib.getExe' config.programs.hyprland.package "hyprctl"} dispatch 'hl.dsp.dpms({ action = \"enable\" })'";
|
after_sleep_cmd = "${hyprctl} dispatch dpms on";
|
||||||
};
|
};
|
||||||
|
|
||||||
listener = [
|
listener = [
|
||||||
# Lock on idle.
|
# Lock on idle.
|
||||||
{
|
{
|
||||||
timeout = 300;
|
timeout = cfg.lockTimeout;
|
||||||
on-timeout = "loginctl lock-session";
|
on-timeout = "loginctl lock-session";
|
||||||
}
|
}
|
||||||
|
# Power the displays off a little later, restoring them on any activity.
|
||||||
|
{
|
||||||
|
timeout = cfg.screenOffTimeout;
|
||||||
|
on-timeout = "${hyprctl} dispatch dpms off";
|
||||||
|
on-resume = "${hyprctl} dispatch dpms on";
|
||||||
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,54 +2,44 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# The lock screen: a hyprlock session-lock surface the compositor owns.
|
# The lock screen: a hyprlock session-lock surface the compositor owns.
|
||||||
let
|
let
|
||||||
sleepIcon = util.glyph [
|
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() {
|
||||||
${lib.getExe' pkgs.procps "pkill"} -RTMIN+9 waybar >/dev/null 2>&1 || true
|
${pkgs.procps}/bin/pkill -RTMIN+9 waybar >/dev/null 2>&1 || true
|
||||||
}
|
}
|
||||||
|
|
||||||
if ${lib.getExe' pkgs.systemd "systemctl"} --user is-active --quiet "$unit"; then
|
if ${pkgs.systemd}/bin/systemctl --user is-active --quiet "$unit"; then
|
||||||
${lib.getExe' pkgs.systemd "systemctl"} --user stop "$unit"
|
${pkgs.systemd}/bin/systemctl --user stop "$unit"
|
||||||
refresh_waybar
|
refresh_waybar
|
||||||
${lib.getExe' pkgs.swayosd "swayosd-client"} --custom-message "${sleepIcon}"
|
${pkgs.swayosd}/bin/swayosd-client --custom-message "${sleepIcon}"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ${lib.getExe' pkgs.systemd "systemd-run"} \
|
if ${pkgs.systemd}/bin/systemd-run \
|
||||||
--user \
|
--user \
|
||||||
--collect \
|
--collect \
|
||||||
--unit=lockscreen-idle-inhibit \
|
--unit=lockscreen-idle-inhibit \
|
||||||
--description="Disable idle-triggered screen locking" \
|
--description="Disable idle-triggered screen locking" \
|
||||||
${lib.getExe' pkgs.systemd "systemd-inhibit"} \
|
${pkgs.systemd}/bin/systemd-inhibit \
|
||||||
--what=idle \
|
--what=idle \
|
||||||
--who=lockscreen-hotkey \
|
--who=lockscreen-hotkey \
|
||||||
--why="lock screen disabled by hotkey" \
|
--why="lock screen disabled by hotkey" \
|
||||||
${lib.getExe' pkgs.coreutils "sleep"} infinity >/dev/null; then
|
${pkgs.coreutils}/bin/sleep infinity >/dev/null; then
|
||||||
refresh_waybar
|
refresh_waybar
|
||||||
${lib.getExe' pkgs.swayosd "swayosd-client"} --custom-message "${sleepOffIcon}"
|
${pkgs.swayosd}/bin/swayosd-client --custom-message "${sleepOffIcon}"
|
||||||
fi
|
|
||||||
'';
|
|
||||||
|
|
||||||
idleLockStatus = pkgs.writeShellScript "waybar-idle-lock-status" ''
|
|
||||||
if ${lib.getExe' pkgs.systemd "systemctl"} --user is-active --quiet lockscreen-idle-inhibit.service; then
|
|
||||||
printf '{"text":"${sleepOffIcon}","tooltip":"Idle locking inhibited","class":"inhibited"}\n'
|
|
||||||
else
|
|
||||||
printf '{"text":"","class":"idle"}\n'
|
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
@@ -58,7 +48,7 @@ in
|
|||||||
lib.mkEnableOption "the hyprlock lock screen";
|
lib.mkEnableOption "the hyprlock lock screen";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.session.hyprland.hyprlock.enable {
|
config = lib.mkIf config.modules.desktop.session.hyprland.hyprlock.enable {
|
||||||
home-manager.users.${config.user.name} = {
|
home-manager.users.${user} = {
|
||||||
# Colors and the lock-screen background come from Stylix's hyprlock target,
|
# Colors and the lock-screen background come from Stylix's hyprlock target,
|
||||||
# so only geometry and behaviour are set here.
|
# so only geometry and behaviour are set here.
|
||||||
programs.hyprlock = {
|
programs.hyprlock = {
|
||||||
@@ -91,36 +81,14 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
registry.keybind.${config.networking.hostName} = {
|
# Lock on Super+X.
|
||||||
hyprlock-lock = {
|
# The guard drops the keypress when a locker is already up, so a second
|
||||||
keys = "SUPER + X";
|
# hyprlock never stacks over the first.
|
||||||
action = {
|
wayland.windowManager.hyprland.settings.bind = [
|
||||||
kind = "exec";
|
"$mod, X, exec, ${pkgs.procps}/bin/pidof hyprlock || ${hyprlock}"
|
||||||
# The guard drops the call when a locker is already up, so no manual
|
"$mod CTRL, X, exec, ${idleLockToggle}"
|
||||||
# trigger stacks a second hyprlock over the first.
|
];
|
||||||
command = "${lib.getExe' pkgs.procps "pidof"} hyprlock || ${lib.getExe pkgs.hyprlock}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
hyprlock-toggle-idle-inhibit = {
|
|
||||||
keys = "SUPER + CTRL + X";
|
|
||||||
action = {
|
|
||||||
kind = "exec";
|
|
||||||
command = "${idleLockToggle}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.waybar.${config.networking.hostName}."custom/idle-lock" = {
|
|
||||||
order = 20;
|
|
||||||
settings = {
|
|
||||||
return-type = "json";
|
|
||||||
exec = "${idleLockStatus}";
|
|
||||||
interval = "once";
|
|
||||||
signal = 9;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,9 @@
|
|||||||
config = lib.mkIf config.modules.desktop.session.login.enable {
|
config = lib.mkIf config.modules.desktop.session.login.enable {
|
||||||
services.greetd = {
|
services.greetd = {
|
||||||
enable = true;
|
enable = true;
|
||||||
settings.default_session.command = lib.escapeShellArgs [
|
settings.default_session.command =
|
||||||
(lib.getExe pkgs.tuigreet)
|
"${lib.getExe pkgs.tuigreet} --time --remember "
|
||||||
"--time"
|
+ "--cmd 'uwsm start -e -D Hyprland hyprland.desktop'";
|
||||||
"--remember"
|
|
||||||
"--cmd"
|
|
||||||
"${lib.getExe config.programs.uwsm.package} start -e -D Hyprland hyprland.desktop"
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,29 +2,19 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# mako as the notification daemon: toasts, a do-not-disturb mode, and history recall.
|
# mako as the notification daemon: toasts, a do-not-disturb mode, and history recall.
|
||||||
let
|
let
|
||||||
dndStatus = pkgs.writeShellScript "waybar-dnd-status" ''
|
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
|
|
||||||
'';
|
|
||||||
|
|
||||||
dndToggle = pkgs.writeShellScript "waybar-dnd-toggle" ''
|
makoctl = "${pkgs.mako}/bin/makoctl";
|
||||||
${lib.getExe' pkgs.mako "makoctl"} mode -t dnd >/dev/null 2>&1
|
|
||||||
${lib.getExe' pkgs.procps "pkill"} -RTMIN+8 waybar
|
|
||||||
'';
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.session.mako.enable = lib.mkEnableOption "the mako notification daemon";
|
options.modules.desktop.session.mako.enable = lib.mkEnableOption "the mako notification daemon";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.session.mako.enable {
|
config = lib.mkIf config.modules.desktop.session.mako.enable {
|
||||||
home-manager.users.${config.user.name} = {
|
home-manager.users.${user} = {
|
||||||
# Colors and the popup font come from Stylix's mako target, so only
|
# Colors and the popup font come from Stylix's mako target, so only
|
||||||
# behaviour is set here.
|
# behaviour is set here.
|
||||||
services.mako = {
|
services.mako = {
|
||||||
@@ -41,25 +31,13 @@ in
|
|||||||
"mode=dnd".invisible = true;
|
"mode=dnd".invisible = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
registry.keybind.${config.networking.hostName}.mako-history = {
|
# Recall the last notification from history, keyboard-only like the rest
|
||||||
keys = "SUPER + N";
|
# of the session.
|
||||||
action = {
|
# $mod is defined by the compositor config these binds share.
|
||||||
kind = "exec";
|
wayland.windowManager.hyprland.settings.bind = [
|
||||||
command = "${lib.getExe' pkgs.mako "makoctl"} restore";
|
"$mod, N, exec, ${makoctl} restore"
|
||||||
};
|
];
|
||||||
};
|
|
||||||
|
|
||||||
registry.waybar.${config.networking.hostName}."custom/dnd" = {
|
|
||||||
order = 80;
|
|
||||||
settings = {
|
|
||||||
return-type = "json";
|
|
||||||
exec = "${dndStatus}";
|
|
||||||
on-click = "${dndToggle}";
|
|
||||||
interval = "once";
|
|
||||||
signal = 8;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,19 @@
|
|||||||
}:
|
}:
|
||||||
# rofi as the desktop launcher and dmenu-style menu frontend.
|
# rofi as the desktop launcher and dmenu-style menu frontend.
|
||||||
let
|
let
|
||||||
|
user = config.user.name;
|
||||||
|
|
||||||
# A session menu built on the same themed rofi, so the power actions look
|
# A session menu built on the same themed rofi, so the power actions look
|
||||||
# like every other menu the launcher drives.
|
# like every other menu the launcher drives.
|
||||||
powerMenu = pkgs.writeShellScript "rofi-power-menu" ''
|
powerMenu = pkgs.writeShellScript "rofi-power-menu" ''
|
||||||
chosen=$(printf '%s\n' Lock Suspend 'Log out' Reboot 'Shut down' \
|
chosen=$(printf '%s\n' Lock Suspend 'Log out' Reboot 'Shut down' \
|
||||||
| ${lib.getExe pkgs.rofi} -dmenu -i -p Power)
|
| ${pkgs.rofi}/bin/rofi -dmenu -i -p Power)
|
||||||
case "$chosen" in
|
case "$chosen" in
|
||||||
Lock) ${lib.getExe' pkgs.systemd "loginctl"} lock-session ;;
|
Lock) ${pkgs.systemd}/bin/loginctl lock-session ;;
|
||||||
Suspend) ${lib.getExe' pkgs.systemd "systemctl"} suspend ;;
|
Suspend) ${pkgs.systemd}/bin/systemctl suspend ;;
|
||||||
'Log out') ${lib.getExe' pkgs.systemd "loginctl"} terminate-session "$XDG_SESSION_ID" ;;
|
'Log out') ${pkgs.systemd}/bin/loginctl terminate-session "$XDG_SESSION_ID" ;;
|
||||||
Reboot) ${lib.getExe' pkgs.systemd "systemctl"} reboot ;;
|
Reboot) ${pkgs.systemd}/bin/systemctl reboot ;;
|
||||||
'Shut down') ${lib.getExe' pkgs.systemd "systemctl"} poweroff ;;
|
'Shut down') ${pkgs.systemd}/bin/systemctl poweroff ;;
|
||||||
esac
|
esac
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
@@ -24,43 +26,26 @@ in
|
|||||||
options.modules.desktop.session.rofi.enable = lib.mkEnableOption "the rofi launcher";
|
options.modules.desktop.session.rofi.enable = lib.mkEnableOption "the rofi launcher";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.session.rofi.enable {
|
config = lib.mkIf config.modules.desktop.session.rofi.enable {
|
||||||
home-manager.users.${config.user.name} = hm: {
|
home-manager.users.${user} = {
|
||||||
# Stylix supplies the Nord theme.
|
# No theme is set here: Stylix's rofi target supplies the Nord one.
|
||||||
programs.rofi = {
|
programs.rofi = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
# Application launch is the only mode, kept minimal so the prompt opens
|
# Application launch is the only mode, kept minimal so the prompt opens
|
||||||
# as fast as rofi can read the desktop entries.
|
# as fast as rofi can read the desktop entries.
|
||||||
# Icons are off: resolving one per entry is the bulk of drun's startup.
|
# Icons are off: resolving one per entry is the bulk of drun's startup.
|
||||||
settings = {
|
extraConfig = {
|
||||||
font = lib.mkForce "JetBrains Mono 12";
|
|
||||||
modi = "drun";
|
modi = "drun";
|
||||||
show-icons = false;
|
show-icons = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
theme.window = {
|
|
||||||
border = hm.config.lib.formats.rasi.mkLiteral "2px";
|
|
||||||
border-color = hm.config.lib.formats.rasi.mkLiteral "#FFFFFF";
|
|
||||||
border-radius = hm.config.lib.formats.rasi.mkLiteral "8px";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.keybind.${config.networking.hostName} = {
|
|
||||||
rofi-launcher = {
|
|
||||||
keys = "SUPER + R";
|
|
||||||
action = {
|
|
||||||
kind = "exec";
|
|
||||||
command = "${lib.getExe pkgs.rofi} -show drun";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
rofi-power-menu = {
|
# The launcher and the session menu, kept next to the tool they drive.
|
||||||
keys = "SUPER + SHIFT + X";
|
# $mod is defined by the compositor config these binds share.
|
||||||
action = {
|
wayland.windowManager.hyprland.settings.bind = [
|
||||||
kind = "exec";
|
"$mod, R, exec, ${pkgs.rofi}/bin/rofi -show drun"
|
||||||
command = "${powerMenu}";
|
"$mod SHIFT, X, exec, ${powerMenu}"
|
||||||
};
|
];
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,19 +6,97 @@
|
|||||||
}:
|
}:
|
||||||
# Waybar: a top status bar reading system state at a glance.
|
# Waybar: a top status bar reading system state at a glance.
|
||||||
let
|
let
|
||||||
moduleNames =
|
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
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
moduleEntries = lib.mapAttrsToList (name: module: {
|
# A Nerd Font Private-Use-Area codepoint as a literal character, decoded
|
||||||
inherit name module;
|
# through JSON so the glyph survives as bytes rather than an editor paste.
|
||||||
}) modules;
|
g = code: builtins.fromJSON ''"\u${code}"'';
|
||||||
|
user = config.user.name;
|
||||||
|
|
||||||
modules = lib.attrByPath [ config.networking.hostName ] { } config.registry.waybar;
|
sleepOffIcon = builtins.fromJSON ''"\udb81\udcb3"'';
|
||||||
|
|
||||||
|
# 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";
|
||||||
@@ -28,35 +106,65 @@ in
|
|||||||
# codepoints Stylix's monospace font does not carry.
|
# codepoints Stylix's monospace font does not carry.
|
||||||
fonts.packages = [ pkgs.nerd-fonts.symbols-only ];
|
fonts.packages = [ pkgs.nerd-fonts.symbols-only ];
|
||||||
|
|
||||||
home-manager.users.${config.user.name}.programs.waybar = {
|
home-manager.users.${user}.programs.waybar = {
|
||||||
enable = true;
|
enable = true;
|
||||||
package = pkgs.alexion.waybar;
|
|
||||||
|
|
||||||
# A user service bound to graphical-session.target, which uwsm activates,
|
# A user service bound to graphical-session.target, which uwsm activates,
|
||||||
# so the bar comes up with the session and no compositor exec-once.
|
# so the bar comes up with the session and no compositor exec-once.
|
||||||
systemd.enable = true;
|
systemd.enable = true;
|
||||||
|
|
||||||
settings.mainBar = lib.mkMerge [
|
style = ''
|
||||||
{
|
.modules-right {
|
||||||
layer = "top";
|
margin-right: 8px;
|
||||||
position = "top";
|
|
||||||
height = 30;
|
|
||||||
spacing = 8;
|
|
||||||
margin-right = 8;
|
|
||||||
|
|
||||||
modules-left = moduleNames "left";
|
|
||||||
modules-center = moduleNames "center";
|
|
||||||
modules-right = moduleNames "right";
|
|
||||||
}
|
}
|
||||||
(lib.mapAttrs (_name: module: module.settings) modules)
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.waybar.${config.networking.hostName} = {
|
#custom-dnd {
|
||||||
clock = {
|
margin-right: 8px;
|
||||||
position = "center";
|
}
|
||||||
order = 10;
|
'';
|
||||||
settings = {
|
|
||||||
|
settings.mainBar = {
|
||||||
|
layer = "top";
|
||||||
|
position = "top";
|
||||||
|
height = 30;
|
||||||
|
spacing = 8;
|
||||||
|
|
||||||
|
modules-left = [ "hyprland/workspaces" ];
|
||||||
|
modules-center = [ "clock" ];
|
||||||
|
modules-right = [
|
||||||
|
"custom/recording"
|
||||||
|
"custom/idle-lock"
|
||||||
|
"mpris"
|
||||||
|
"wireplumber"
|
||||||
|
]
|
||||||
|
++ lib.optional vpnClient.enable "custom/vpn"
|
||||||
|
++ [
|
||||||
|
"custom/network"
|
||||||
|
"battery"
|
||||||
|
"custom/dnd"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Per-application icons for the windows on each workspace, mapped from
|
||||||
|
# window class with a neutral glyph for anything unlisted.
|
||||||
|
"hyprland/workspaces" = {
|
||||||
|
on-click = "activate";
|
||||||
|
format = "{id} {windows}";
|
||||||
|
format-window-separator = " ";
|
||||||
|
window-rewrite-default = g "f2d0";
|
||||||
|
window-rewrite = {
|
||||||
|
"class<Alacritty>" = g "f120";
|
||||||
|
"class<com.mitchellh.ghostty>" = g "f120";
|
||||||
|
"class<.*[Ff]irefox.*>" = g "f269";
|
||||||
|
"class<chromium.*>" = g "f268";
|
||||||
|
"class<[Cc]ode>" = g "f121";
|
||||||
|
"class<obsidian>" = g "f02d";
|
||||||
|
"class<[Ss]ignal>" = g "f4ad";
|
||||||
|
"class<[Ss]team>" = g "f1b6";
|
||||||
|
"class<[Vv]esktop>" = g "f1ff";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
clock = {
|
||||||
format = "{:%a %d %b %H:%M}";
|
format = "{:%a %d %b %H:%M}";
|
||||||
tooltip-format = "<tt>{calendar}</tt>";
|
tooltip-format = "<tt>{calendar}</tt>";
|
||||||
calendar = {
|
calendar = {
|
||||||
@@ -65,6 +173,77 @@ in
|
|||||||
format.today = "<b><u>{}</u></b>";
|
format.today = "<b><u>{}</u></b>";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mpris = {
|
||||||
|
format = "${g "f001"} {title}";
|
||||||
|
format-paused = "${g "f04c"} {title}";
|
||||||
|
max-length = 40;
|
||||||
|
on-click = "play-pause";
|
||||||
|
on-scroll-up = "next";
|
||||||
|
on-scroll-down = "previous";
|
||||||
|
};
|
||||||
|
|
||||||
|
wireplumber = {
|
||||||
|
format = "{icon} {volume}%";
|
||||||
|
format-muted = "${g "f026"} muted";
|
||||||
|
format-icons = [
|
||||||
|
(g "f026")
|
||||||
|
(g "f027")
|
||||||
|
(g "f028")
|
||||||
|
];
|
||||||
|
on-click = "${pkgs.wireplumber}/bin/wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle";
|
||||||
|
};
|
||||||
|
|
||||||
|
"custom/vpn" = {
|
||||||
|
return-type = "json";
|
||||||
|
exec = "${vpnStatus}";
|
||||||
|
interval = 5;
|
||||||
|
};
|
||||||
|
|
||||||
|
"custom/network" = {
|
||||||
|
return-type = "json";
|
||||||
|
exec = "${networkStatus}";
|
||||||
|
interval = 5;
|
||||||
|
};
|
||||||
|
|
||||||
|
battery = {
|
||||||
|
format = "{icon} {capacity}%";
|
||||||
|
format-charging = "${g "f0e7"} {capacity}%";
|
||||||
|
format-icons = [
|
||||||
|
(g "f244")
|
||||||
|
(g "f243")
|
||||||
|
(g "f242")
|
||||||
|
(g "f241")
|
||||||
|
(g "f240")
|
||||||
|
];
|
||||||
|
states = {
|
||||||
|
warning = 20;
|
||||||
|
critical = 10;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
"custom/dnd" = {
|
||||||
|
return-type = "json";
|
||||||
|
exec = "${dndStatus}";
|
||||||
|
on-click = "${dndToggle}";
|
||||||
|
interval = "once";
|
||||||
|
signal = 8;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Recording state has no event to hook, so the widget samples the
|
||||||
|
# wf-recorder process once a second.
|
||||||
|
"custom/recording" = {
|
||||||
|
return-type = "json";
|
||||||
|
exec = "${recStatus}";
|
||||||
|
interval = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
"custom/idle-lock" = {
|
||||||
|
return-type = "json";
|
||||||
|
exec = "${idleLockStatus}";
|
||||||
|
interval = "once";
|
||||||
|
signal = 9;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,30 +1,5 @@
|
|||||||
{
|
{ config, lib, ... }:
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
util,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
# PipeWire as the desktop audio server.
|
# PipeWire as the desktop audio server.
|
||||||
let
|
|
||||||
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
|
|
||||||
{
|
{
|
||||||
options.modules.desktop.utilities.audio.enable = lib.mkEnableOption "the PipeWire audio server";
|
options.modules.desktop.utilities.audio.enable = lib.mkEnableOption "the PipeWire audio server";
|
||||||
|
|
||||||
@@ -37,50 +12,5 @@ in
|
|||||||
alsa.enable = true;
|
alsa.enable = true;
|
||||||
pulse.enable = true;
|
pulse.enable = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
registry.keybind.${config.networking.hostName} = {
|
|
||||||
volume-up = volumeKey "XF86AudioRaiseVolume" "raise --max-volume 100";
|
|
||||||
volume-down = volumeKey "XF86AudioLowerVolume" "lower";
|
|
||||||
mute = {
|
|
||||||
keys = "XF86AudioMute";
|
|
||||||
locked = true;
|
|
||||||
action = {
|
|
||||||
kind = "exec";
|
|
||||||
command = "${lib.getExe' pkgs.swayosd "swayosd-client"} --output-volume mute-toggle";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
media-play = mediaKey "XF86AudioPlay" "play-pause";
|
|
||||||
media-next = mediaKey "XF86AudioNext" "next";
|
|
||||||
media-previous = mediaKey "XF86AudioPrev" "previous";
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.waybar.${config.networking.hostName} = {
|
|
||||||
mpris = {
|
|
||||||
order = 30;
|
|
||||||
settings = {
|
|
||||||
format = "${util.glyph "f001"} {title}";
|
|
||||||
format-paused = "${util.glyph "f04c"} {title}";
|
|
||||||
max-length = 40;
|
|
||||||
on-click = "play-pause";
|
|
||||||
on-scroll-up = "next";
|
|
||||||
on-scroll-down = "previous";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
wireplumber = {
|
|
||||||
order = 40;
|
|
||||||
settings = {
|
|
||||||
format = "{icon} {volume}%";
|
|
||||||
format-muted = "${util.glyph "f026"} muted";
|
|
||||||
format-icons = [
|
|
||||||
(util.glyph "f026")
|
|
||||||
(util.glyph "f027")
|
|
||||||
(util.glyph "f028")
|
|
||||||
];
|
|
||||||
on-click = "${lib.getExe' pkgs.wireplumber "wpctl"} set-mute @DEFAULT_AUDIO_SINK@ toggle";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,21 @@
|
|||||||
}:
|
}:
|
||||||
# Clipboard history: cliphist records every copy, picked back through rofi.
|
# Clipboard history: cliphist records every copy, picked back through rofi.
|
||||||
let
|
let
|
||||||
|
user = config.user.name;
|
||||||
|
|
||||||
|
cliphist = "${pkgs.cliphist}/bin/cliphist";
|
||||||
|
rofi = "${pkgs.rofi}/bin/rofi";
|
||||||
|
wl-copy = "${pkgs.wl-clipboard}/bin/wl-copy";
|
||||||
|
|
||||||
# The picker reuses the themed rofi, so history looks like every other menu
|
# The picker reuses the themed rofi, so history looks like every other menu
|
||||||
# the launcher drives.
|
# the launcher drives.
|
||||||
# decode is needed because list emits id-prefixed lines rather than the copied
|
# decode is needed because list emits id-prefixed lines rather than the copied
|
||||||
# bytes, so the chosen id has to be resolved back before it can be re-copied.
|
# bytes, so the chosen id has to be resolved back before it can be re-copied.
|
||||||
picker = pkgs.writeShellScript "clipboard-picker" ''
|
picker = pkgs.writeShellScript "clipboard-picker" ''
|
||||||
${lib.getExe pkgs.cliphist} list \
|
${cliphist} list \
|
||||||
| ${lib.getExe pkgs.rofi} -dmenu -i -p Clipboard \
|
| ${rofi} -dmenu -i -p Clipboard \
|
||||||
| ${lib.getExe pkgs.cliphist} decode \
|
| ${cliphist} decode \
|
||||||
| ${lib.getExe' pkgs.wl-clipboard "wl-copy"}
|
| ${wl-copy}
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@@ -22,7 +28,7 @@ in
|
|||||||
lib.mkEnableOption "cliphist clipboard history";
|
lib.mkEnableOption "cliphist clipboard history";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.utilities.clipboard.enable {
|
config = lib.mkIf config.modules.desktop.utilities.clipboard.enable {
|
||||||
home-manager.users.${config.user.name} = {
|
home-manager.users.${user} = {
|
||||||
# wl-copy and wl-paste on PATH, so the shell can pipe into and out of the
|
# wl-copy and wl-paste on PATH, so the shell can pipe into and out of the
|
||||||
# clipboard.
|
# clipboard.
|
||||||
# The watchers and picker above reach wl-clipboard by store path, so this
|
# The watchers and picker above reach wl-clipboard by store path, so this
|
||||||
@@ -32,14 +38,11 @@ in
|
|||||||
# Two watchers record text and images to history, bound to the graphical
|
# Two watchers record text and images to history, bound to the graphical
|
||||||
# session so uwsm starts and stops them with it.
|
# session so uwsm starts and stops them with it.
|
||||||
services.cliphist.enable = true;
|
services.cliphist.enable = true;
|
||||||
};
|
|
||||||
|
|
||||||
registry.keybind.${config.networking.hostName}.clipboard-history = {
|
# $mod is defined by the compositor config these binds share.
|
||||||
keys = "SUPER + SHIFT + V";
|
wayland.windowManager.hyprland.settings.bind = [
|
||||||
action = {
|
"$mod SHIFT, V, exec, ${picker}"
|
||||||
kind = "exec";
|
];
|
||||||
command = "${picker}";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# SwayOSD: a transient on-screen popup for volume and brightness changes.
|
# SwayOSD: a transient on-screen popup for volume and brightness changes.
|
||||||
|
let
|
||||||
|
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";
|
||||||
@@ -16,41 +19,21 @@
|
|||||||
# group write, so the server dims the panel without root.
|
# group write, so the server dims the panel without root.
|
||||||
# Membership below grants the running session that access.
|
# Membership below grants the running session that access.
|
||||||
services.udev.packages = [ pkgs.swayosd ];
|
services.udev.packages = [ pkgs.swayosd ];
|
||||||
users.users.${config.user.name}.extraGroups = [ "video" ];
|
users.users.${user}.extraGroups = [ "video" ];
|
||||||
|
|
||||||
# The server draws the popups, so it runs for the whole graphical session.
|
# The server draws the popups, so it runs for the whole graphical session.
|
||||||
# It binds to the target uwsm activates.
|
# It binds to the target uwsm activates.
|
||||||
home-manager.users.${config.user.name}.systemd.user.services.swayosd = {
|
home-manager.users.${user}.systemd.user.services.swayosd = {
|
||||||
Unit = {
|
Unit = {
|
||||||
Description = "SwayOSD on-screen display server";
|
Description = "SwayOSD on-screen display server";
|
||||||
PartOf = [ "graphical-session.target" ];
|
PartOf = [ "graphical-session.target" ];
|
||||||
After = [ "graphical-session.target" ];
|
After = [ "graphical-session.target" ];
|
||||||
};
|
};
|
||||||
Service = {
|
Service = {
|
||||||
ExecStart = lib.getExe' pkgs.swayosd "swayosd-server";
|
ExecStart = "${pkgs.swayosd}/bin/swayosd-server";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
};
|
};
|
||||||
Install.WantedBy = [ "graphical-session.target" ];
|
Install.WantedBy = [ "graphical-session.target" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
registry.keybind.${config.networking.hostName} = {
|
|
||||||
brightness-up = {
|
|
||||||
keys = "XF86MonBrightnessUp";
|
|
||||||
repeating = true;
|
|
||||||
action = {
|
|
||||||
kind = "exec";
|
|
||||||
command = "${lib.getExe' pkgs.swayosd "swayosd-client"} --brightness raise";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
brightness-down = {
|
|
||||||
keys = "XF86MonBrightnessDown";
|
|
||||||
repeating = true;
|
|
||||||
action = {
|
|
||||||
kind = "exec";
|
|
||||||
command = "${lib.getExe' pkgs.swayosd "swayosd-client"} --brightness lower";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,44 +2,41 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Keybound screen recording: pick a region, then toggle a video-only capture.
|
# Keybound screen recording: pick a region, then toggle a video-only capture.
|
||||||
let
|
let
|
||||||
# A single key starts and stops recording.
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
if ${lib.getExe' pkgs.procps "pgrep"} -x wf-recorder >/dev/null; then
|
slurp = "${pkgs.slurp}/bin/slurp";
|
||||||
${lib.getExe' pkgs.procps "pkill"} -INT -x wf-recorder
|
wf-recorder = "${pkgs.wf-recorder}/bin/wf-recorder";
|
||||||
|
notify-send = "${pkgs.libnotify}/bin/notify-send";
|
||||||
|
pgrep = "${pkgs.procps}/bin/pgrep";
|
||||||
|
pkill = "${pkgs.procps}/bin/pkill";
|
||||||
|
date = "${pkgs.coreutils}/bin/date";
|
||||||
|
mkdir = "${pkgs.coreutils}/bin/mkdir";
|
||||||
|
xdgUserDir = "${pkgs.xdg-user-dirs}/bin/xdg-user-dir";
|
||||||
|
|
||||||
|
# A single key both starts and stops.
|
||||||
|
# A running wf-recorder is stopped with SIGINT so it finalises the file.
|
||||||
|
# Otherwise slurp picks a region and wf-recorder runs in the foreground until
|
||||||
|
# that stop arrives, so this instance lives for the whole recording and then
|
||||||
|
# reports it saved.
|
||||||
|
# No -a means no audio is captured.
|
||||||
|
toggle = pkgs.writeShellScript "screen-record-toggle" ''
|
||||||
|
if ${pgrep} -x wf-recorder >/dev/null; then
|
||||||
|
${pkill} -INT -x wf-recorder
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
region=$(${lib.getExe pkgs.slurp}) || exit 0
|
region=$(${slurp}) || exit 0
|
||||||
dir="$(${lib.getExe' pkgs.xdg-user-dirs "xdg-user-dir"} VIDEOS)/Recordings"
|
dir="$(${xdgUserDir} VIDEOS)/Recordings"
|
||||||
${lib.getExe' pkgs.coreutils "mkdir"} -p "$dir"
|
${mkdir} -p "$dir"
|
||||||
file="$dir/recording-$(${lib.getExe' pkgs.coreutils "date"} +%Y%m%d-%H%M%S).mp4"
|
file="$dir/recording-$(${date} +%Y%m%d-%H%M%S).mp4"
|
||||||
|
|
||||||
${lib.getExe' pkgs.libnotify "notify-send"} -a "Screen recording" "Recording started" "Region capture, no audio."
|
${notify-send} -a "Screen recording" "Recording started" "Region capture, no audio."
|
||||||
${lib.getExe pkgs.wf-recorder} -g "$region" -f "$file" &
|
${wf-recorder} -g "$region" -f "$file"
|
||||||
recorder=$!
|
${notify-send} -a "Screen recording" "Recording saved" "$file"
|
||||||
refresh_waybar
|
|
||||||
wait "$recorder" || true
|
|
||||||
refresh_waybar
|
|
||||||
${lib.getExe' pkgs.libnotify "notify-send"} -a "Screen recording" "Recording saved" "$file"
|
|
||||||
'';
|
|
||||||
|
|
||||||
# Empty text collapses the widget when recording is idle.
|
|
||||||
recordingStatus = pkgs.writeShellScript "waybar-recording-status" ''
|
|
||||||
if ${lib.getExe' pkgs.procps "pgrep"} -x wf-recorder >/dev/null; then
|
|
||||||
printf '{"text":"${util.glyph "f03d"}","tooltip":"Recording","class":"recording"}\n'
|
|
||||||
else
|
|
||||||
printf '{"text":"","class":"idle"}\n'
|
|
||||||
fi
|
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@@ -47,22 +44,11 @@ in
|
|||||||
lib.mkEnableOption "wf-recorder screen recording";
|
lib.mkEnableOption "wf-recorder screen recording";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.utilities.recording.enable {
|
config = lib.mkIf config.modules.desktop.utilities.recording.enable {
|
||||||
registry.keybind.${config.networking.hostName}.recording-toggle = {
|
home-manager.users.${user} = {
|
||||||
keys = "SUPER + SHIFT + R";
|
# $mod is defined by the compositor config these binds share.
|
||||||
action = {
|
wayland.windowManager.hyprland.settings.bind = [
|
||||||
kind = "exec";
|
"$mod SHIFT, R, exec, ${toggle}"
|
||||||
command = "${recordingToggle}";
|
];
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.waybar.${config.networking.hostName}."custom/recording" = {
|
|
||||||
order = 10;
|
|
||||||
settings = {
|
|
||||||
return-type = "json";
|
|
||||||
exec = "${recordingStatus}";
|
|
||||||
interval = "once";
|
|
||||||
signal = 10;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,26 +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
|
||||||
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="$(${lib.getExe' pkgs.xdg-user-dirs "xdg-user-dir"} PICTURES)/Screenshots"
|
dir="$(${xdgUserDir} PICTURES)/Screenshots"
|
||||||
${lib.getExe' pkgs.coreutils "mkdir"} -p "$dir"
|
mkdir -p "$dir"
|
||||||
${lib.getExe pkgs.grimblast} save ${target} - \
|
${grimblast} save ${target} - \
|
||||||
| ${lib.getExe' pkgs.coreutils "tee"} >(${lib.getExe' pkgs.wl-clipboard "wl-copy"}) \
|
| ${tee} >(${wl-copy}) \
|
||||||
| ${lib.getExe pkgs.satty} --filename - \
|
| ${satty} --filename - \
|
||||||
--output-filename "$dir/screenshot-%Y%m%d-%H%M%S.png" \
|
--output-filename "$dir/screenshot-%Y%m%d-%H%M%S.png" \
|
||||||
--copy-command ${lib.getExe' pkgs.wl-clipboard "wl-copy"} \
|
--copy-command ${wl-copy} \
|
||||||
--save-after-copy \
|
--save-after-copy \
|
||||||
--early-exit \
|
--early-exit \
|
||||||
--actions-on-enter save-to-clipboard
|
--actions-on-enter save-to-clipboard
|
||||||
@@ -37,21 +37,23 @@ in
|
|||||||
lib.mkEnableOption "screenshots via grimblast and satty";
|
lib.mkEnableOption "screenshots via grimblast and satty";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.utilities.screenshot.enable {
|
config = lib.mkIf config.modules.desktop.utilities.screenshot.enable {
|
||||||
home-manager.users.${config.user.name} = {
|
home-manager.users.${user} = {
|
||||||
# satty is a one-shot annotation surface, so float it rather than letting
|
# satty is a one-shot annotation surface, so float it rather than letting
|
||||||
# it claim a tile in the layout.
|
# it claim a tile in the layout.
|
||||||
wayland.windowManager.hyprland.settings.window_rule = [
|
# windowrule v3 syntax: space-separated field/value tokens, comma
|
||||||
{
|
# separated, with matchers under a match: prefix and effects bare.
|
||||||
match.class = "^(com\\.gabm\\.satty)$";
|
wayland.windowManager.hyprland.settings.windowrule = [
|
||||||
float = true;
|
"float 1, match:class ^(com\\.gabm\\.satty)$"
|
||||||
}
|
|
||||||
];
|
];
|
||||||
};
|
|
||||||
|
|
||||||
registry.keybind.${config.networking.hostName} = {
|
# Print with plain/Shift/Ctrl for region/window/full.
|
||||||
screenshot-area = screenshotKey "Print" "area";
|
# Super+L is already the hjkl focus and movement bind, so screenshots take
|
||||||
screenshot-active = screenshotKey "SHIFT + Print" "active";
|
# the Print key instead.
|
||||||
screenshot-screen = screenshotKey "CTRL + Print" "screen";
|
wayland.windowManager.hyprland.settings.bind = [
|
||||||
|
", Print, exec, ${capture "area"}"
|
||||||
|
"SHIFT, Print, exec, ${capture "active"}"
|
||||||
|
"CTRL, Print, exec, ${capture "screen"}"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,15 @@
|
|||||||
}:
|
}:
|
||||||
# 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
|
||||||
|
|
||||||
# A single static Nord wallpaper, drawn as a vertical gradient across the
|
# A single static Nord wallpaper, drawn as a vertical gradient across the
|
||||||
# Polar Night shades so it needs no committed binary and no network fetch.
|
# Polar Night shades so it needs no committed binary and no network fetch.
|
||||||
wallpaper = pkgs.runCommand "nord-wallpaper.png" { } ''
|
wallpaper = pkgs.runCommand "nord-wallpaper.png" { } ''
|
||||||
${lib.getExe pkgs.imagemagick} \
|
${pkgs.imagemagick}/bin/magick \
|
||||||
-size 2560x1440 gradient:'#2E3440'-'#3B4252' \
|
-size 2560x1440 gradient:'#2E3440'-'#3B4252' \
|
||||||
"$out"
|
"$out"
|
||||||
'';
|
'';
|
||||||
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.utilities.theming.enable =
|
options.modules.desktop.utilities.theming.enable =
|
||||||
@@ -57,7 +59,7 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
home-manager.users.${config.user.name} = {
|
home-manager.users.${user} = {
|
||||||
# Stylix drives the cursor through home-manager's pointer-cursor config,
|
# Stylix drives the cursor through home-manager's pointer-cursor config,
|
||||||
# whose generation must be switched on explicitly.
|
# whose generation must be switched on explicitly.
|
||||||
home.pointerCursor.enable = true;
|
home.pointerCursor.enable = true;
|
||||||
@@ -66,10 +68,5 @@ in
|
|||||||
# base16 mapping Stylix would apply, so its target stays off.
|
# base16 mapping Stylix would apply, so its target stays off.
|
||||||
stylix.targets.nixvim.enable = false;
|
stylix.targets.nixvim.enable = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
registry.env.${config.networking.hostName} = {
|
|
||||||
HYPRCURSOR_THEME = config.stylix.cursor.name;
|
|
||||||
HYPRCURSOR_SIZE = toString config.stylix.cursor.size;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,14 @@
|
|||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
# Declares XDG user directories, so tools that query them use predictable
|
# Declares the XDG user directories, so a tool that queries them lands in a
|
||||||
# folders a host can relocate from one place.
|
# well-known folder a host can relocate from one place.
|
||||||
let
|
let
|
||||||
homeDirectory = "/home/${config.user.name}";
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.utilities.userdirs.enable = lib.mkEnableOption "XDG user directories";
|
options.modules.desktop.utilities.userdirs.enable = lib.mkEnableOption "XDG user directories";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.desktop.utilities.userdirs.enable {
|
config = lib.mkIf config.modules.desktop.utilities.userdirs.enable {
|
||||||
home-manager.users.${config.user.name}.xdg.userDirs = {
|
# Write ~/.config/user-dirs.dirs from the option defaults.
|
||||||
enable = true;
|
home-manager.users.${user}.xdg.userDirs.enable = true;
|
||||||
createDirectories = true;
|
|
||||||
setSessionVariables = true;
|
|
||||||
|
|
||||||
desktop = "${homeDirectory}/.ignoreme";
|
|
||||||
documents = "${homeDirectory}/docs";
|
|
||||||
download = "${homeDirectory}/dwn";
|
|
||||||
music = "${homeDirectory}/mus";
|
|
||||||
pictures = "${homeDirectory}/pic";
|
|
||||||
publicShare = "${homeDirectory}/.ignoreme";
|
|
||||||
templates = "${homeDirectory}/.ignoreme";
|
|
||||||
videos = "${homeDirectory}/vid";
|
|
||||||
projects = "${homeDirectory}/.ignoreme";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ in
|
|||||||
modules.network.ssh.userKey.sopsFile = hostSecrets;
|
modules.network.ssh.userKey.sopsFile = hostSecrets;
|
||||||
|
|
||||||
services.xserver.xkb.layout = "us";
|
services.xserver.xkb.layout = "us";
|
||||||
|
services.xserver.xkb.options = "caps:escape_shifted_capslock";
|
||||||
console.useXkbConfig = true;
|
console.useXkbConfig = true;
|
||||||
|
|
||||||
sops.defaultSopsFile = ../../secrets/shared.yaml;
|
sops.defaultSopsFile = ../../secrets/shared.yaml;
|
||||||
@@ -53,7 +54,6 @@ in
|
|||||||
home.username = config.user.name;
|
home.username = config.user.name;
|
||||||
home.homeDirectory = "/home/${config.user.name}";
|
home.homeDirectory = "/home/${config.user.name}";
|
||||||
home.stateVersion = config.system.stateVersion;
|
home.stateVersion = config.system.stateVersion;
|
||||||
home.sessionVariables = lib.attrByPath [ config.networking.hostName ] { } config.registry.env;
|
|
||||||
home.packages = with pkgs; [
|
home.packages = with pkgs; [
|
||||||
eza
|
eza
|
||||||
unzip
|
unzip
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
}:
|
}:
|
||||||
# Imports existing ZFS pools that hold durable service state.
|
# Imports existing ZFS pools that hold durable service state.
|
||||||
let
|
let
|
||||||
cfg = config.modules.host.zfs;
|
cfg = config.modules.host.zfs; # NOLINT(let_order)
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.host.zfs = {
|
options.modules.host.zfs = {
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
util,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
# NetworkManager-managed networking.
|
|
||||||
let
|
|
||||||
networkStatus = pkgs.writeShellScript "waybar-network-status" ''
|
|
||||||
active=$(
|
|
||||||
${lib.getExe' pkgs.networkmanager "nmcli"} -t -f DEVICE,TYPE,STATE,CONNECTION device status \
|
|
||||||
| ${lib.getExe' pkgs.gawk "awk"} -F: '$3 == "connected" && $2 != "loopback" { print; exit }'
|
|
||||||
)
|
|
||||||
|
|
||||||
if [ -z "$active" ]; then
|
|
||||||
${lib.getExe pkgs.jq} -cn \
|
|
||||||
--arg text "${util.glyph "f127"} off" \
|
|
||||||
--arg tooltip "Network disconnected" \
|
|
||||||
--arg class "disconnected" \
|
|
||||||
'{ text: $text, tooltip: $tooltip, class: $class }'
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
IFS=: read -r device type _state connection <<< "$active"
|
|
||||||
|
|
||||||
case "$type" in
|
|
||||||
wifi) icon="${util.glyph "f1eb"}" ;;
|
|
||||||
ethernet) icon="LAN" ;;
|
|
||||||
*) icon="NET" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
ipaddr=$(${lib.getExe' pkgs.iproute2 "ip"} -4 -o addr show dev "$device" | ${lib.getExe' pkgs.gawk "awk"} '{ print $4; exit }')
|
|
||||||
|
|
||||||
${lib.getExe pkgs.jq} -cn \
|
|
||||||
--arg text "$icon $connection" \
|
|
||||||
--arg tooltip "$device: ''${ipaddr:-no IPv4}" \
|
|
||||||
--arg class "$type" \
|
|
||||||
'{ text: $text, tooltip: $tooltip, class: $class }'
|
|
||||||
'';
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options.modules.network.enable = lib.mkEnableOption "NetworkManager-managed networking";
|
|
||||||
|
|
||||||
config = lib.mkIf config.modules.network.enable {
|
|
||||||
networking.networkmanager.enable = true;
|
|
||||||
|
|
||||||
registry.waybar.${config.networking.hostName}."custom/network" = {
|
|
||||||
order = 60;
|
|
||||||
settings = {
|
|
||||||
return-type = "json";
|
|
||||||
exec = "${networkStatus}";
|
|
||||||
interval = 5;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -7,17 +7,19 @@
|
|||||||
# Configures inbound and outbound SSH access.
|
# Configures inbound and outbound SSH access.
|
||||||
let
|
let
|
||||||
cfg = config.modules.network.ssh;
|
cfg = config.modules.network.ssh;
|
||||||
user = config.user.name;
|
user = config.user.name; # NOLINT(let_order)
|
||||||
|
|
||||||
|
# NOLINT(let_order)
|
||||||
workstationNames = [
|
workstationNames = [
|
||||||
"neogaia"
|
"neogaia"
|
||||||
"zeus"
|
"zeus"
|
||||||
];
|
];
|
||||||
serverNames = [ "pikachu" ];
|
serverNames = [ "pikachu" ];
|
||||||
|
|
||||||
isWorkstation = lib.elem config.networking.hostName workstationNames;
|
isWorkstation = lib.elem config.networking.hostName workstationNames; # NOLINT(let_order)
|
||||||
isServer = lib.elem config.networking.hostName serverNames;
|
isServer = lib.elem config.networking.hostName serverNames; # NOLINT(let_order)
|
||||||
|
|
||||||
|
# NOLINT(let_order)
|
||||||
clientKeyFor = host: builtins.readFile (../../hosts + "/${host}/ssh_user_ed25519_key.pub");
|
clientKeyFor = host: builtins.readFile (../../hosts + "/${host}/ssh_user_ed25519_key.pub");
|
||||||
hostKeysFor =
|
hostKeysFor =
|
||||||
host:
|
host:
|
||||||
@@ -26,6 +28,7 @@ let
|
|||||||
(../../hosts + "/${host}/ssh_host_rsa_key.pub")
|
(../../hosts + "/${host}/ssh_host_rsa_key.pub")
|
||||||
];
|
];
|
||||||
|
|
||||||
|
# NOLINT(let_order)
|
||||||
managedTargets = lib.genAttrs (workstationNames ++ serverNames) (name: {
|
managedTargets = lib.genAttrs (workstationNames ++ serverNames) (name: {
|
||||||
hostName = registry.address.${name}.self;
|
hostName = registry.address.${name}.self;
|
||||||
clientKey = clientKeyFor name;
|
clientKey = clientKeyFor name;
|
||||||
@@ -45,6 +48,7 @@ let
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# NOLINT(let_order)
|
||||||
mkTarget = name: target: {
|
mkTarget = name: target: {
|
||||||
hostName = target.hostName or name;
|
hostName = target.hostName or name;
|
||||||
user = target.user or user;
|
user = target.user or user;
|
||||||
@@ -53,14 +57,15 @@ let
|
|||||||
clientKey = target.clientKey or null;
|
clientKey = target.clientKey or null;
|
||||||
hostKeys = target.hostKeys or [ ];
|
hostKeys = target.hostKeys or [ ];
|
||||||
};
|
};
|
||||||
targets = lib.mapAttrs mkTarget (managedTargets // extraTargets);
|
targets = lib.mapAttrs mkTarget (managedTargets // extraTargets); # NOLINT(let_order)
|
||||||
|
|
||||||
clientKeysFor = names: lib.filter (key: key != null) (map (name: targets.${name}.clientKey) names);
|
clientKeysFor = names: lib.filter (key: key != null) (map (name: targets.${name}.clientKey) names); # NOLINT(let_order)
|
||||||
|
# NOLINT(let_order)
|
||||||
defaultAuthorizedKeys = lib.flatten (
|
defaultAuthorizedKeys = lib.flatten (
|
||||||
clientKeysFor workstationNames ++ lib.optional isServer (clientKeysFor serverNames)
|
clientKeysFor workstationNames ++ lib.optional isServer (clientKeysFor serverNames)
|
||||||
);
|
);
|
||||||
|
|
||||||
outboundTargetNames =
|
outboundTargetNames = # NOLINT(let_order)
|
||||||
let
|
let
|
||||||
groupTargets =
|
groupTargets =
|
||||||
if isServer then
|
if isServer then
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# Configures bridges for tagged VLANs on a physical interface that uses DHCP.
|
# Configures bridges for tagged VLANs on a physical interface that uses DHCP.
|
||||||
let
|
let
|
||||||
cfg = config.modules.network.vlans;
|
cfg = config.modules.network.vlans; # NOLINT(let_order)
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.network.vlans = {
|
options.modules.network.vlans = {
|
||||||
|
|||||||
@@ -1,34 +1,10 @@
|
|||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.modules.network.vpn-client;
|
cfg = config.modules.network.vpn-client; # NOLINT(let_order)
|
||||||
|
|
||||||
vpnIcon = util.glyph [
|
|
||||||
"db83"
|
|
||||||
"dc9d"
|
|
||||||
];
|
|
||||||
|
|
||||||
vpnStatus = pkgs.writeShellScript "waybar-vpn-status" ''
|
|
||||||
interface=${lib.escapeShellArg cfg.interface}
|
|
||||||
|
|
||||||
if ! ${lib.getExe' pkgs.iproute2 "ip"} link show dev "$interface" >/dev/null 2>&1; then
|
|
||||||
printf '{"text":"","class":"idle"}\n'
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
ipaddr=$(${lib.getExe' pkgs.iproute2 "ip"} -4 -o addr show dev "$interface" | ${lib.getExe' pkgs.gawk "awk"} '{ print $4; exit }')
|
|
||||||
|
|
||||||
${lib.getExe pkgs.jq} -cn \
|
|
||||||
--arg text "${vpnIcon}" \
|
|
||||||
--arg tooltip "VPN $interface: ''${ipaddr:-no IPv4}" \
|
|
||||||
--arg class "active" \
|
|
||||||
'{ text: $text, tooltip: $tooltip, class: $class }'
|
|
||||||
'';
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.network.vpn-client = {
|
options.modules.network.vpn-client = {
|
||||||
@@ -37,31 +13,61 @@ in
|
|||||||
interface = lib.mkOption {
|
interface = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "wg-home";
|
default = "wg-home";
|
||||||
description = "Names the WireGuard interface.";
|
description = "WireGuard interface name.";
|
||||||
};
|
};
|
||||||
|
|
||||||
address = lib.mkOption {
|
address = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
example = "10.23.100.3/32";
|
example = "10.23.100.3/32";
|
||||||
description = "Sets the client tunnel address in CIDR notation.";
|
description = "Client tunnel address in CIDR notation.";
|
||||||
|
};
|
||||||
|
|
||||||
|
privateKeySecret = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "wireguard-home-private-key";
|
||||||
|
description = "SOPS secret containing the WireGuard client private key.";
|
||||||
|
};
|
||||||
|
|
||||||
|
autoStart = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Whether to start the VPN tunnel during boot.";
|
||||||
|
};
|
||||||
|
|
||||||
|
mtu = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.int;
|
||||||
|
default = 1280;
|
||||||
|
description = "Tunnel MTU, or null to use the WireGuard default.";
|
||||||
};
|
};
|
||||||
|
|
||||||
sopsFile = lib.mkOption {
|
sopsFile = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.path;
|
type = lib.types.nullOr lib.types.path;
|
||||||
default = null;
|
default = null;
|
||||||
description = "Names the encrypted file that contains the WireGuard private key.";
|
description = "Encrypted secrets file that contains privateKeySecret.";
|
||||||
};
|
};
|
||||||
|
|
||||||
peer = {
|
peer = {
|
||||||
publicKey = lib.mkOption {
|
publicKey = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Sets the WireGuard public key for the remote peer.";
|
description = "WireGuard public key for the remote peer.";
|
||||||
};
|
};
|
||||||
|
|
||||||
endpoint = lib.mkOption {
|
endpoint = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
example = "203.0.113.10:51820";
|
example = "203.0.113.10:51820";
|
||||||
description = "Sets the WireGuard endpoint for the remote peer.";
|
description = "WireGuard endpoint for the remote peer.";
|
||||||
|
};
|
||||||
|
|
||||||
|
allowedIPs = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
|
default = [ "10.23.0.0/16" ];
|
||||||
|
description = "Routes to send through the VPN tunnel.";
|
||||||
|
};
|
||||||
|
|
||||||
|
persistentKeepalive = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.int;
|
||||||
|
default = 25;
|
||||||
|
description = "Persistent keepalive interval in seconds, or null to disable it.";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -70,39 +76,30 @@ in
|
|||||||
assertions = [
|
assertions = [
|
||||||
{
|
{
|
||||||
assertion = cfg.sopsFile != null;
|
assertion = cfg.sopsFile != null;
|
||||||
message = "Set modules.network.vpn-client.sopsFile when the VPN client is enabled.";
|
message = "modules.network.vpn-client.sopsFile must name the encrypted secrets file containing the WireGuard private key.";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
sops.secrets.wireguard-home-private-key.sopsFile = cfg.sopsFile;
|
sops.secrets.${cfg.privateKeySecret}.sopsFile = cfg.sopsFile;
|
||||||
|
|
||||||
registry.shell-shortcut.${config.networking.hostName} = {
|
systemd.targets."wireguard-${cfg.interface}" = lib.mkIf (!cfg.autoStart) {
|
||||||
vpn-on = "sudo systemctl start wireguard-${cfg.interface}.target";
|
wantedBy = lib.mkForce [ ];
|
||||||
vpn-off = "sudo systemctl stop 'wireguard-${cfg.interface}*.service' wireguard-${cfg.interface}.target";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registry.waybar.${config.networking.hostName}."custom/vpn" = {
|
|
||||||
order = 50;
|
|
||||||
settings = {
|
|
||||||
return-type = "json";
|
|
||||||
exec = "${vpnStatus}";
|
|
||||||
interval = 5;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.targets."wireguard-${cfg.interface}".wantedBy = lib.mkForce [ ];
|
|
||||||
|
|
||||||
networking.wireguard.interfaces.${cfg.interface} = {
|
networking.wireguard.interfaces.${cfg.interface} = {
|
||||||
ips = [ cfg.address ];
|
ips = [ cfg.address ];
|
||||||
mtu = 1280;
|
inherit (cfg) mtu;
|
||||||
privateKeyFile = config.sops.secrets.wireguard-home-private-key.path;
|
privateKeyFile = config.sops.secrets.${cfg.privateKeySecret}.path;
|
||||||
|
|
||||||
peers = [
|
peers = [
|
||||||
{
|
(
|
||||||
inherit (cfg.peer) publicKey endpoint;
|
{
|
||||||
allowedIPs = [ "10.23.0.0/16" ];
|
inherit (cfg.peer) publicKey endpoint allowedIPs;
|
||||||
persistentKeepalive = 25;
|
}
|
||||||
}
|
// lib.optionalAttrs (cfg.peer.persistentKeepalive != null) {
|
||||||
|
inherit (cfg.peer) persistentKeepalive;
|
||||||
|
}
|
||||||
|
)
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
62
configs/nixos/modules/services/actualbudget.nix
Normal file
62
configs/nixos/modules/services/actualbudget.nix
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.modules.services.actualbudget; # 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,135 +2,147 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
util,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.modules.services.copyparty;
|
cfg = config.modules.services.copyparty; # NOLINT(let_order)
|
||||||
|
|
||||||
accountName = config.user.name;
|
volumeType = lib.types.submodule {
|
||||||
|
options = {
|
||||||
|
path = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "Filesystem path served by this volume.";
|
||||||
|
};
|
||||||
|
|
||||||
serviceGroup = "storage";
|
access = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
|
||||||
|
default = { };
|
||||||
|
example = {
|
||||||
|
A = [ "alexion" ];
|
||||||
|
};
|
||||||
|
description = "Copyparty access map from permission string to account names.";
|
||||||
|
};
|
||||||
|
|
||||||
serviceStateDir = "/var/lib/copyparty";
|
readOnly = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Serve this volume read-only.";
|
||||||
|
};
|
||||||
|
|
||||||
serviceUser = "copyparty";
|
flags = lib.mkOption {
|
||||||
|
type = lib.types.listOf lib.types.str;
|
||||||
writableVolumes = lib.filter (volume: !volume.readOnly) (lib.attrValues cfg.volumes);
|
default = [ ];
|
||||||
|
example = [ "dthumb" ];
|
||||||
port = 3923;
|
description = "Copyparty volume flags.";
|
||||||
defaultAccess = {
|
};
|
||||||
A = [ accountName ];
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
defaultWritableFlags = {
|
configScript = pkgs.writeShellScript "copyparty-config" ''
|
||||||
d2d = true;
|
set -eu
|
||||||
d2t = true;
|
|
||||||
dthumb = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
credentialHash = builtins.substring 0 16 (builtins.hashString "sha256" accountName);
|
umask 077
|
||||||
|
password="$(${pkgs.coreutils}/bin/tr -d '\n' < "$CREDENTIALS_DIRECTORY/password")"
|
||||||
|
|
||||||
credentialName = "password-${credentialHash}";
|
cat > /run/copyparty/copyparty.conf <<EOF
|
||||||
|
[global]
|
||||||
|
p: ${toString cfg.port}
|
||||||
|
usernames
|
||||||
|
http-only
|
||||||
|
no-crt
|
||||||
|
no-thumb
|
||||||
|
no-snap
|
||||||
|
no-rescan
|
||||||
|
ses-db: ${cfg.stateDir}/sessions.db
|
||||||
|
chpw-db: ${cfg.stateDir}/chpw.json
|
||||||
|
shr-db: ${cfg.stateDir}/shares.db
|
||||||
|
${renderedReverseProxy}
|
||||||
|
[accounts]
|
||||||
|
${cfg.accountName}: $password
|
||||||
|
|
||||||
passwordPlaceholder = "@COPYPARTY_PASSWORD_${credentialHash}@";
|
${renderedVolumes}
|
||||||
|
EOF
|
||||||
globalSettings = {
|
|
||||||
i = "0.0.0.0";
|
|
||||||
usernames = true;
|
|
||||||
"http-only" = true;
|
|
||||||
"no-crt" = true;
|
|
||||||
"no-thumb" = true;
|
|
||||||
"no-snap" = true;
|
|
||||||
"no-rescan" = true;
|
|
||||||
"ses-db" = "${serviceStateDir}/sessions.db";
|
|
||||||
"chpw-db" = "${serviceStateDir}/chpw.json";
|
|
||||||
"shr-db" = "${serviceStateDir}/shares.db";
|
|
||||||
}
|
|
||||||
// lib.optionalAttrs cfg.reverseProxy.enable {
|
|
||||||
rproxy = 1;
|
|
||||||
"xff-hdr" = "x-forwarded-for";
|
|
||||||
};
|
|
||||||
|
|
||||||
globalExtraConfig = lib.optionalString cfg.reverseProxy.enable (
|
|
||||||
lib.concatMapStringsSep "\n" (source: "xff-src: ${source}") cfg.reverseProxy.trustedSources
|
|
||||||
);
|
|
||||||
|
|
||||||
volumeFlags =
|
|
||||||
flags:
|
|
||||||
lib.optionalAttrs (flags != { }) (
|
|
||||||
{
|
|
||||||
chmod_d = "2770";
|
|
||||||
chmod_f = "660";
|
|
||||||
}
|
|
||||||
// flags
|
|
||||||
);
|
|
||||||
|
|
||||||
renderedVolumes = lib.mapAttrs (_urlPath: volume: {
|
|
||||||
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;
|
|
||||||
|
|
||||||
configText = util.formats.copyparty.render {
|
|
||||||
inherit globalExtraConfig;
|
|
||||||
accounts.${accountName} = passwordPlaceholder;
|
|
||||||
global = globalSettings;
|
|
||||||
volumes = renderedVolumes;
|
|
||||||
};
|
|
||||||
|
|
||||||
configFile = pkgs.writeText "copyparty.conf" configText;
|
|
||||||
|
|
||||||
replacePassword = ''
|
|
||||||
${lib.getExe pkgs.replace-secret} ${lib.escapeShellArg passwordPlaceholder} "$CREDENTIALS_DIRECTORY/${credentialName}" /run/copyparty/copyparty.conf
|
|
||||||
'';
|
'';
|
||||||
|
renderAccess = permission: accounts: " ${permission}: ${lib.concatStringsSep ", " accounts}";
|
||||||
|
|
||||||
|
hasFlag =
|
||||||
|
name: flags: lib.any (flag: lib.hasPrefix "${name}:" flag || lib.hasPrefix "${name}=" flag) flags;
|
||||||
|
|
||||||
|
defaultPermissionFlags =
|
||||||
|
flags:
|
||||||
|
lib.optionals (!(hasFlag "chmod_d" flags)) [ "chmod_d: 2770" ]
|
||||||
|
++ lib.optionals (!(hasFlag "chmod_f" flags)) [ "chmod_f: 660" ];
|
||||||
|
|
||||||
|
renderFlags =
|
||||||
|
flags: lib.concatMapStringsSep "\n" (flag: " ${flag}") (flags ++ defaultPermissionFlags flags);
|
||||||
|
|
||||||
|
renderVolume = urlPath: volume: ''
|
||||||
|
[${urlPath}]
|
||||||
|
${volume.path}
|
||||||
|
accs:
|
||||||
|
${lib.concatStringsSep "\n" (lib.mapAttrsToList renderAccess volume.access)}
|
||||||
|
${lib.optionalString (volume.flags != [ ]) ''
|
||||||
|
flags:
|
||||||
|
${renderFlags volume.flags}
|
||||||
|
''}
|
||||||
|
'';
|
||||||
|
|
||||||
|
renderTrustedSource = source: " xff-src: ${source}";
|
||||||
|
|
||||||
|
renderedReverseProxy = lib.optionalString cfg.reverseProxy.enable ''
|
||||||
|
rproxy: 1
|
||||||
|
xff-hdr: x-forwarded-for
|
||||||
|
${lib.concatStringsSep "\n" (map renderTrustedSource cfg.reverseProxy.trustedSources)}
|
||||||
|
'';
|
||||||
|
|
||||||
|
renderedVolumes = lib.concatStringsSep "\n" (lib.mapAttrsToList renderVolume cfg.volumes);
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.services.copyparty = {
|
options.modules.services.copyparty = {
|
||||||
enable = lib.mkEnableOption "Copyparty";
|
enable = lib.mkEnableOption "Copyparty";
|
||||||
|
|
||||||
passwordFile = lib.mkOption {
|
package = lib.mkPackageOption pkgs "copyparty-min" { };
|
||||||
|
|
||||||
|
port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
default = 3923;
|
||||||
|
description = "Port Copyparty listens on.";
|
||||||
|
};
|
||||||
|
|
||||||
|
user = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Sets the runtime file that contains the Copyparty account password.";
|
default = "copyparty";
|
||||||
|
description = "User account that runs Copyparty.";
|
||||||
|
};
|
||||||
|
|
||||||
|
group = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "storage";
|
||||||
|
description = "Group account that owns Copyparty's writable state and served files.";
|
||||||
|
};
|
||||||
|
|
||||||
|
accountName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "alexion";
|
||||||
|
description = "Copyparty account name configured for password authentication.";
|
||||||
|
};
|
||||||
|
|
||||||
|
passwordFile = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
description = "Runtime file containing the Copyparty account password.";
|
||||||
|
};
|
||||||
|
|
||||||
|
stateDir = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "/var/lib/copyparty";
|
||||||
|
description = "Directory where Copyparty stores service state.";
|
||||||
};
|
};
|
||||||
|
|
||||||
volumes = lib.mkOption {
|
volumes = lib.mkOption {
|
||||||
type = lib.types.attrsOf (
|
type = lib.types.attrsOf volumeType;
|
||||||
lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
path = lib.mkOption {
|
|
||||||
type = lib.types.externalPath;
|
|
||||||
description = "Sets the filesystem path served by this volume.";
|
|
||||||
};
|
|
||||||
|
|
||||||
access = lib.mkOption {
|
|
||||||
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
|
|
||||||
default = { };
|
|
||||||
example = {
|
|
||||||
A = [ "alexion" ];
|
|
||||||
};
|
|
||||||
description = "Maps Copyparty permission strings to account names.";
|
|
||||||
};
|
|
||||||
|
|
||||||
readOnly = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = false;
|
|
||||||
description = "Serves this volume read-only.";
|
|
||||||
};
|
|
||||||
|
|
||||||
flags = lib.mkOption {
|
|
||||||
type = lib.types.attrs;
|
|
||||||
default = { };
|
|
||||||
example = {
|
|
||||||
dthumb = true;
|
|
||||||
};
|
|
||||||
description = "Sets Copyparty volume flags.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
default = { };
|
default = { };
|
||||||
description = "Defines Copyparty volumes by URL path.";
|
description = "Copyparty volumes keyed by their URL path.";
|
||||||
};
|
};
|
||||||
|
|
||||||
reverseProxy = {
|
reverseProxy = {
|
||||||
@@ -139,7 +151,7 @@ in
|
|||||||
trustedSources = lib.mkOption {
|
trustedSources = lib.mkOption {
|
||||||
type = lib.types.listOf lib.types.str;
|
type = lib.types.listOf lib.types.str;
|
||||||
default = [ ];
|
default = [ ];
|
||||||
description = "Lists proxy sources that can set Copyparty's forwarded client IP header.";
|
description = "IP ranges that may supply Copyparty's forwarded client IP header.";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -161,49 +173,46 @@ in
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
users.users.${serviceUser} = {
|
users.users.${cfg.user} = {
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
uid = config.registry."stable-uid".${config.networking.hostName}.copyparty;
|
uid = config.registry."stable-uid".${config.networking.hostName}.copyparty;
|
||||||
group = serviceGroup;
|
inherit (cfg) group;
|
||||||
home = serviceStateDir;
|
home = cfg.stateDir;
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.copyparty = {
|
systemd.services.copyparty = {
|
||||||
description = "Copyparty file server";
|
description = "Copyparty file server";
|
||||||
after = [ "network.target" ];
|
after = [ "network.target" ];
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
preStart = ''
|
|
||||||
${lib.getExe' pkgs.coreutils "install"} -m 600 ${configFile} /run/copyparty/copyparty.conf
|
|
||||||
${replacePassword}
|
|
||||||
'';
|
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ReloadSignal = "SIGUSR1";
|
ReloadSignal = "SIGUSR1";
|
||||||
Type = "simple";
|
Type = "simple";
|
||||||
User = serviceUser;
|
User = cfg.user;
|
||||||
Group = serviceGroup;
|
Group = cfg.group;
|
||||||
WorkingDirectory = serviceStateDir;
|
WorkingDirectory = cfg.stateDir;
|
||||||
Environment = [
|
Environment = [
|
||||||
"HOME=${serviceStateDir}"
|
"HOME=${cfg.stateDir}"
|
||||||
"XDG_CONFIG_HOME=${serviceStateDir}"
|
"XDG_CONFIG_HOME=${cfg.stateDir}"
|
||||||
];
|
];
|
||||||
StateDirectory = "copyparty";
|
StateDirectory = "copyparty";
|
||||||
RuntimeDirectory = "copyparty";
|
RuntimeDirectory = "copyparty";
|
||||||
RuntimeDirectoryMode = "0700";
|
RuntimeDirectoryMode = "0700";
|
||||||
LoadCredential = [ "${credentialName}:${cfg.passwordFile}" ];
|
LoadCredential = [ "password:${cfg.passwordFile}" ];
|
||||||
ExecStart = "${lib.getExe pkgs.copyparty-min} -c /run/copyparty/copyparty.conf";
|
ExecStartPre = configScript;
|
||||||
|
ExecStart = "${lib.getExe cfg.package} -c /run/copyparty/copyparty.conf";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
NoNewPrivileges = true;
|
NoNewPrivileges = true;
|
||||||
PrivateTmp = true;
|
PrivateTmp = true;
|
||||||
ProtectSystem = "strict";
|
ProtectSystem = "strict";
|
||||||
ProtectHome = true;
|
ProtectHome = true;
|
||||||
ReadWritePaths = [
|
ReadWritePaths = [
|
||||||
serviceStateDir
|
cfg.stateDir
|
||||||
]
|
]
|
||||||
++ map (volume: volume.path) writableVolumes;
|
++ map (volume: volume.path) (lib.filter (volume: !volume.readOnly) (lib.attrValues cfg.volumes));
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ port ];
|
networking.firewall.allowedTCPPorts = [ cfg.port ];
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,33 +92,33 @@
|
|||||||
startScript = pkgs.writeShellScript "palworld-start" ''
|
startScript = pkgs.writeShellScript "palworld-start" ''
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
${lib.getExe' pkgs.coreutils "install"} -d \
|
${pkgs.coreutils}/bin/install -d \
|
||||||
-m 0770 \
|
-m 0770 \
|
||||||
${lib.escapeShellArg service.stateDir} \
|
${lib.escapeShellArg service.stateDir} \
|
||||||
${lib.escapeShellArg (builtins.dirOf savedDir)}
|
${lib.escapeShellArg (builtins.dirOf savedDir)}
|
||||||
|
|
||||||
if [ -L ${lib.escapeShellArg savedDir} ]; then
|
if [ -L ${lib.escapeShellArg savedDir} ]; then
|
||||||
current_target="$(${lib.getExe' pkgs.coreutils "readlink"} ${lib.escapeShellArg savedDir})"
|
current_target="$(${pkgs.coreutils}/bin/readlink ${lib.escapeShellArg savedDir})"
|
||||||
if [ "$current_target" != ${lib.escapeShellArg service.stateDir} ]; then
|
if [ "$current_target" != ${lib.escapeShellArg service.stateDir} ]; then
|
||||||
${lib.getExe' pkgs.coreutils "rm"} ${lib.escapeShellArg savedDir}
|
${pkgs.coreutils}/bin/rm ${lib.escapeShellArg savedDir}
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -d ${lib.escapeShellArg savedDir} ] && [ ! -L ${lib.escapeShellArg savedDir} ]; then
|
if [ -d ${lib.escapeShellArg savedDir} ] && [ ! -L ${lib.escapeShellArg savedDir} ]; then
|
||||||
if ${lib.getExe' pkgs.findutils "find"} ${lib.escapeShellArg service.stateDir} -mindepth 1 -maxdepth 1 | ${lib.getExe' pkgs.gnugrep "grep"} -q .; then
|
if ${pkgs.findutils}/bin/find ${lib.escapeShellArg service.stateDir} -mindepth 1 -maxdepth 1 | ${pkgs.gnugrep}/bin/grep -q .; then
|
||||||
${lib.getExe' pkgs.coreutils "rm"} -rf ${lib.escapeShellArg savedDir}
|
${pkgs.coreutils}/bin/rm -rf ${lib.escapeShellArg savedDir}
|
||||||
else
|
else
|
||||||
${lib.getExe' pkgs.coreutils "cp"} -a ${lib.escapeShellArg savedDir}/. ${lib.escapeShellArg service.stateDir}/
|
${pkgs.coreutils}/bin/cp -a ${lib.escapeShellArg savedDir}/. ${lib.escapeShellArg service.stateDir}/
|
||||||
${lib.getExe' pkgs.coreutils "rm"} -rf ${lib.escapeShellArg savedDir}
|
${pkgs.coreutils}/bin/rm -rf ${lib.escapeShellArg savedDir}
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -e ${lib.escapeShellArg savedDir} ]; then
|
if [ ! -e ${lib.escapeShellArg savedDir} ]; then
|
||||||
${lib.getExe' pkgs.coreutils "ln"} -s ${lib.escapeShellArg service.stateDir} ${lib.escapeShellArg savedDir}
|
${pkgs.coreutils}/bin/ln -s ${lib.escapeShellArg service.stateDir} ${lib.escapeShellArg savedDir}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
password="$(${lib.getExe' pkgs.coreutils "tr"} -d '\n' < ${lib.escapeShellArg service.passwordFile})"
|
password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.passwordFile})"
|
||||||
admin_password="$(${lib.getExe' pkgs.coreutils "tr"} -d '\n' < ${lib.escapeShellArg service.adminPasswordFile})"
|
admin_password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.adminPasswordFile})"
|
||||||
|
|
||||||
exec ./PalServer.sh \
|
exec ./PalServer.sh \
|
||||||
"$@" \
|
"$@" \
|
||||||
@@ -145,12 +145,9 @@
|
|||||||
]
|
]
|
||||||
++ lib.optional service.publicLobby "-publiclobby"
|
++ lib.optional service.publicLobby "-publiclobby"
|
||||||
++ lib.optionals (service.publicIP != null) [ "-publicip=${service.publicIP}" ]
|
++ lib.optionals (service.publicIP != null) [ "-publicip=${service.publicIP}" ]
|
||||||
++ lib.optionals service.rcon.enable [
|
++ lib.optional service.rcon.enable "-rcon";
|
||||||
"-rcon"
|
|
||||||
"-RCONPort=${toString service.rcon.port}"
|
|
||||||
];
|
|
||||||
udpPorts = [ service.port ];
|
udpPorts = [ service.port ];
|
||||||
tcpPorts = lib.optional service.rcon.enable service.rcon.port;
|
tcpPorts = [ service.port ];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
startScript = pkgs.writeShellScript "valheim-start" ''
|
startScript = pkgs.writeShellScript "valheim-start" ''
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
password="$(${lib.getExe' pkgs.coreutils "tr"} -d '\n' < ${lib.escapeShellArg service.passwordFile})"
|
password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.passwordFile})"
|
||||||
exec ./valheim_server.x86_64 "$@" -password "$password"
|
exec ./valheim_server.x86_64 "$@" -password "$password"
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.modules.services.homepage;
|
cfg = config.modules.services.homepage; # NOLINT(let_order)
|
||||||
|
|
||||||
derivedAllowedHosts = lib.unique (
|
derivedAllowedHosts = lib.unique (
|
||||||
lib.optional (selfRoute != null) selfRoute.domain
|
lib.optional (selfRoute != null) selfRoute.domain
|
||||||
@@ -111,8 +111,7 @@ let
|
|||||||
let
|
let
|
||||||
sectionOrder = sectionOrderByTab.${tab} or [ ];
|
sectionOrder = sectionOrderByTab.${tab} or [ ];
|
||||||
in
|
in
|
||||||
lib.filter (section: lib.hasAttr section sections) sectionOrder
|
sectionOrder ++ lib.filter (section: !lib.elem section sectionOrder) (lib.attrNames sections);
|
||||||
++ lib.filter (section: !lib.elem section sectionOrder) (lib.attrNames sections);
|
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.modules.services.navidrome;
|
cfg = config.modules.services.navidrome; # NOLINT(let_order)
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.services.navidrome = {
|
options.modules.services.navidrome = {
|
||||||
|
|||||||
@@ -40,14 +40,43 @@ let
|
|||||||
proxy_pass ${stream.address}:${toString stream.port};
|
proxy_pass ${stream.address}:${toString stream.port};
|
||||||
}
|
}
|
||||||
'') generatedUdpStreams;
|
'') generatedUdpStreams;
|
||||||
generatedVirtualHosts = lib.listToAttrs generatedVirtualHostList;
|
generatedVirtualHosts =
|
||||||
|
if duplicateDomainMessages != [ ] then
|
||||||
|
throw (builtins.head duplicateDomainMessages)
|
||||||
|
else
|
||||||
|
lib.listToAttrs generatedVirtualHostList;
|
||||||
|
|
||||||
# Route registry validation already requires unique domains.
|
duplicateDomainMessages = lib.concatLists (
|
||||||
generatedVirtualHostList = lib.concatLists (
|
lib.mapAttrsToList (
|
||||||
|
domain: virtualHosts:
|
||||||
|
if lib.length virtualHosts > 1 then
|
||||||
|
[
|
||||||
|
"duplicate route domain ${domain} in ${
|
||||||
|
lib.concatMapStringsSep " and " (
|
||||||
|
virtualHost: mkRegistryPath virtualHost.registryName virtualHost.host virtualHost.entry
|
||||||
|
) virtualHosts
|
||||||
|
}"
|
||||||
|
]
|
||||||
|
else
|
||||||
|
[ ]
|
||||||
|
) virtualHostsByDomain
|
||||||
|
);
|
||||||
|
|
||||||
|
virtualHostsByDomain = lib.foldl' (
|
||||||
|
acc: virtualHost:
|
||||||
|
acc
|
||||||
|
// {
|
||||||
|
${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 route.protocol) (
|
lib.mapAttrsToList (entry: _route: mkVirtualHost host entry "https") (
|
||||||
lib.filterAttrs (_entry: route: route.protocol != "udp") routes
|
lib.filterAttrs (_entry: route: route.protocol == "https") routes
|
||||||
)
|
)
|
||||||
) registry.route
|
) registry.route
|
||||||
);
|
);
|
||||||
@@ -64,6 +93,8 @@ let
|
|||||||
inherit (route) port;
|
inherit (route) port;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
inherit host entry;
|
||||||
|
registryName = "route";
|
||||||
name = route.domain;
|
name = route.domain;
|
||||||
value = {
|
value = {
|
||||||
forceSSL = !isHttp;
|
forceSSL = !isHttp;
|
||||||
@@ -77,6 +108,15 @@ let
|
|||||||
|
|
||||||
mkBackend = address: port: "http://${address}:${toString port}";
|
mkBackend = address: port: "http://${address}:${toString port}";
|
||||||
|
|
||||||
|
generatedHttpVirtualHostList = lib.concatLists (
|
||||||
|
lib.mapAttrsToList (
|
||||||
|
host: routes:
|
||||||
|
lib.mapAttrsToList (entry: _route: mkVirtualHost host entry "http") (
|
||||||
|
lib.filterAttrs (_entry: route: route.protocol == "http") routes
|
||||||
|
)
|
||||||
|
) registry.route
|
||||||
|
);
|
||||||
|
|
||||||
mkRegistryPath =
|
mkRegistryPath =
|
||||||
registryName: host: entry:
|
registryName: host: entry:
|
||||||
"registry.${registryName}.${host}.${entry}";
|
"registry.${registryName}.${host}.${entry}";
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
cfg = config.modules.services.romm;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options.modules.services.romm = {
|
|
||||||
enable = lib.mkEnableOption "RomM";
|
|
||||||
|
|
||||||
environmentFile = lib.mkOption {
|
|
||||||
type = lib.types.path;
|
|
||||||
default = "/run/secrets/romm.env";
|
|
||||||
readOnly = true;
|
|
||||||
description = "Path to RomM's environment file.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkMerge [
|
|
||||||
{
|
|
||||||
registry."stable-uid".${config.networking.hostName} = {
|
|
||||||
romm = 20005;
|
|
||||||
"redis-romm" = 20006;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
(lib.mkIf cfg.enable {
|
|
||||||
networking.firewall.allowedTCPPorts = [ 80 ];
|
|
||||||
|
|
||||||
services.romm = {
|
|
||||||
enable = true;
|
|
||||||
group = "storage";
|
|
||||||
database.createLocally = true;
|
|
||||||
redis.createLocally = true;
|
|
||||||
watcher.enable = true;
|
|
||||||
inherit (cfg) environmentFile;
|
|
||||||
};
|
|
||||||
|
|
||||||
users.users = {
|
|
||||||
romm.uid = config.registry."stable-uid".${config.networking.hostName}.romm;
|
|
||||||
redis-romm.uid = config.registry."stable-uid".${config.networking.hostName}."redis-romm";
|
|
||||||
};
|
|
||||||
})
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -15,9 +15,9 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
serviceName = lib.mkOption {
|
serviceName = lib.mkOption {
|
||||||
type = lib.types.strMatching "[a-z0-9][a-z0-9-]*";
|
type = lib.types.str;
|
||||||
example = "valheim";
|
example = "valheim";
|
||||||
description = "Lowercase systemd service basename for the game server.";
|
description = "Name of the systemd service that runs the game server.";
|
||||||
};
|
};
|
||||||
|
|
||||||
appId = lib.mkOption {
|
appId = lib.mkOption {
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
prepareScript = pkgs.writeShellScript "steamcmd-${service.serviceName}-prepare" ''
|
prepareScript = pkgs.writeShellScript "steamcmd-${service.serviceName}-prepare" ''
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
${lib.getExe' pkgs.coreutils "install"} -d \
|
${pkgs.coreutils}/bin/install -d \
|
||||||
-o ${user} \
|
-o ${user} \
|
||||||
-g ${group} \
|
-g ${group} \
|
||||||
-m 0770 \
|
-m 0770 \
|
||||||
@@ -155,7 +155,7 @@
|
|||||||
wantedBy = lib.mkIf service.autoStart [ "multi-user.target" ];
|
wantedBy = lib.mkIf service.autoStart [ "multi-user.target" ];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "oneshot";
|
Type = "oneshot";
|
||||||
ExecStart = "${lib.getExe' pkgs.systemd "systemctl"} --no-block start ${service.serviceName}.service";
|
ExecStart = "${pkgs.systemd}/bin/systemctl --no-block start ${service.serviceName}.service";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,14 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Bazel for the primary user's shell environment.
|
# Bazel for the primary user's shell environment.
|
||||||
|
let
|
||||||
|
user = config.user.name;
|
||||||
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.bazel.enable =
|
options.modules.shell.bazel.enable =
|
||||||
lib.mkEnableOption "Bazel for the primary user's shell environment";
|
lib.mkEnableOption "Bazel for the primary user's shell environment";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.shell.bazel.enable {
|
config = lib.mkIf config.modules.shell.bazel.enable {
|
||||||
home-manager.users.${config.user.name}.home.packages = [ pkgs.bazel ];
|
home-manager.users.${user}.home.packages = [ pkgs.bazel ];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
25
configs/nixos/modules/shell/direnv/default.nix
Normal file
25
configs/nixos/modules/shell/direnv/default.nix
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
# direnv for the primary user, configured through home-manager.
|
||||||
|
let
|
||||||
|
user = config.user.name;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.modules.shell.direnv.enable =
|
||||||
|
lib.mkEnableOption "direnv for the primary user, with nix-direnv shell caching";
|
||||||
|
|
||||||
|
config = lib.mkIf config.modules.shell.direnv.enable {
|
||||||
|
home-manager.users.${user}.programs.direnv = {
|
||||||
|
enable = true;
|
||||||
|
silent = true;
|
||||||
|
|
||||||
|
# nix-direnv caches the evaluated shell so re-entering a directory is instant
|
||||||
|
# instead of re-running `nix develop`, and adds the `use flake` stdlib helper.
|
||||||
|
nix-direnv.enable = true;
|
||||||
|
stdlib = builtins.readFile ./repo-flake.sh;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
33
configs/nixos/modules/shell/direnv/repo-flake.sh
Normal file
33
configs/nixos/modules/shell/direnv/repo-flake.sh
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
if declare -f use_flake >/dev/null; then
|
||||||
|
__dotfiles_use_flake_definition="$(declare -f use_flake)"
|
||||||
|
eval "${__dotfiles_use_flake_definition/use_flake/__dotfiles_nix_direnv_use_flake}"
|
||||||
|
unset __dotfiles_use_flake_definition
|
||||||
|
|
||||||
|
use_flake() {
|
||||||
|
local flake_expr="${1:-.}"
|
||||||
|
|
||||||
|
if [[ "$flake_expr" == //* ]]; then
|
||||||
|
local git_root
|
||||||
|
if ! git_root="$(git rev-parse --show-toplevel 2>/dev/null)"; then
|
||||||
|
log_error 'use flake // requires a Git repository'
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local flake_suffix="${flake_expr#//}"
|
||||||
|
if [[ -n "$flake_suffix" && "$flake_suffix" != \#* && "$flake_suffix" != /* ]]; then
|
||||||
|
flake_expr="path:${git_root}/${flake_suffix}"
|
||||||
|
else
|
||||||
|
flake_expr="path:${git_root}${flake_suffix}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
shift
|
||||||
|
__dotfiles_nix_direnv_use_flake "$flake_expr" "$@"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
__dotfiles_nix_direnv_use_flake "$@"
|
||||||
|
}
|
||||||
|
else
|
||||||
|
log_error 'nix-direnv must define use_flake before the // wrapper loads'
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# fish for the primary user, configured through home-manager.
|
# fish for the primary user, configured through home-manager.
|
||||||
let
|
let
|
||||||
cfg = config.modules.shell.fish;
|
cfg = config.modules.shell.fish; # NOLINT(let_order)
|
||||||
commandsOf = shortcuts: lib.mapAttrs (_name: shortcut: shortcut.command) shortcuts;
|
commandsOf = shortcuts: lib.mapAttrs (_name: shortcut: shortcut.command) shortcuts;
|
||||||
shortcuts = lib.attrByPath [ config.networking.hostName ] { } config.registry.shell-shortcut;
|
shortcuts = lib.attrByPath [ config.networking.hostName ] { } config.registry.shell-shortcut;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
|
|||||||
@@ -24,9 +24,6 @@ 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; [
|
||||||
|
|||||||
@@ -12,14 +12,18 @@ let
|
|||||||
piDir = "${config.users.users.${user}.home}/.pi/agent";
|
piDir = "${config.users.users.${user}.home}/.pi/agent";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.pi.enable =
|
options.modules.shell.pi = {
|
||||||
lib.mkEnableOption "Pi, a terminal coding agent, configured through home-manager";
|
enable = lib.mkEnableOption "Pi, a terminal coding agent, configured through home-manager";
|
||||||
|
};
|
||||||
|
|
||||||
config = lib.mkIf config.modules.shell.pi.enable {
|
config = lib.mkIf 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 = pkgs.alexion.pi-coding-agent;
|
package = pkgs.pi-coding-agent.overrideAttrs {
|
||||||
|
src = ../../../../../third_party/pi;
|
||||||
|
patches = [ ];
|
||||||
|
};
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
defaultProvider = "openai-codex";
|
defaultProvider = "openai-codex";
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ in
|
|||||||
lib.mkEnableOption "Python tools for the primary user's shell environment";
|
lib.mkEnableOption "Python tools for the primary user's shell environment";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.shell.python.enable {
|
config = lib.mkIf config.modules.shell.python.enable {
|
||||||
registry.env.${config.networking.hostName}.PYTHON_TOOLCHAIN_ROOT = "${pkgs.python3}";
|
home-manager.users.${user}.home = {
|
||||||
|
sessionVariables.PYTHON_TOOLCHAIN_ROOT = "${pkgs.python3}";
|
||||||
home-manager.users.${user}.home.packages = [ pkgs.python3 ];
|
packages = [ pkgs.python3 ];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ in
|
|||||||
lib.mkEnableOption "Rust tools for the primary user's shell environment";
|
lib.mkEnableOption "Rust tools for the primary user's shell environment";
|
||||||
|
|
||||||
config = lib.mkIf config.modules.shell.rust.enable {
|
config = lib.mkIf config.modules.shell.rust.enable {
|
||||||
registry.env.${config.networking.hostName}.RUST_TOOLCHAIN_ROOT = "${toolchain}";
|
|
||||||
|
|
||||||
home-manager.users.${user} = {
|
home-manager.users.${user} = {
|
||||||
|
home.sessionVariables.RUST_TOOLCHAIN_ROOT = "${toolchain}";
|
||||||
|
|
||||||
home.packages = [
|
home.packages = [
|
||||||
toolchain
|
toolchain
|
||||||
pkgs.rust-analyzer
|
pkgs.rust-analyzer
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
{ lib, ... }:
|
|
||||||
{
|
|
||||||
type = lib.types.str;
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
{ lib, util, ... }:
|
|
||||||
{
|
|
||||||
checks = [
|
|
||||||
{
|
|
||||||
assertion = util.registry.predicates.uniquePerHost (entry: entry.value.keys);
|
|
||||||
message = "Desktop keybinds must use unique key chords per host.";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
assertion =
|
|
||||||
entries:
|
|
||||||
lib.all (
|
|
||||||
entry:
|
|
||||||
(entry.value.action.kind == "exec" && entry.value.action.command != null)
|
|
||||||
|| (
|
|
||||||
builtins.elem entry.value.action.kind [
|
|
||||||
"focus"
|
|
||||||
"move-window"
|
|
||||||
]
|
|
||||||
&& lib.xor (entry.value.action.direction != null) (entry.value.action.workspace != null)
|
|
||||||
)
|
|
||||||
|| (
|
|
||||||
entry.value.action.kind == "resize-window"
|
|
||||||
&& entry.value.action.direction != null
|
|
||||||
&& entry.value.action.workspace == null
|
|
||||||
)
|
|
||||||
|| (
|
|
||||||
builtins.elem entry.value.action.kind [
|
|
||||||
"toggle-floating"
|
|
||||||
"toggle-fullscreen"
|
|
||||||
"close-window"
|
|
||||||
]
|
|
||||||
&& entry.value.action.command == null
|
|
||||||
&& entry.value.action.direction == null
|
|
||||||
&& entry.value.action.workspace == null
|
|
||||||
)
|
|
||||||
) entries;
|
|
||||||
message = "Desktop keybind actions must set the fields their kind requires.";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
keys = lib.mkOption {
|
|
||||||
description = "Sets the key chord for this desktop keybind.";
|
|
||||||
type = lib.types.str;
|
|
||||||
example = "SUPER + Return";
|
|
||||||
};
|
|
||||||
|
|
||||||
repeating = lib.mkOption {
|
|
||||||
description = "Repeats the keybind action while the key chord is held.";
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = false;
|
|
||||||
example = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
locked = lib.mkOption {
|
|
||||||
description = "Allows the keybind action while the session is locked.";
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = false;
|
|
||||||
example = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
action = lib.mkOption {
|
|
||||||
description = "Declares the action this keybind runs.";
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
kind = lib.mkOption {
|
|
||||||
description = "Selects the keybind action kind.";
|
|
||||||
type = lib.types.enum [
|
|
||||||
"exec"
|
|
||||||
"focus"
|
|
||||||
"move-window"
|
|
||||||
"resize-window"
|
|
||||||
"toggle-floating"
|
|
||||||
"toggle-fullscreen"
|
|
||||||
"close-window"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
command = lib.mkOption {
|
|
||||||
description = "Command executed by an exec keybind.";
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
default = null;
|
|
||||||
example = "ghostty";
|
|
||||||
};
|
|
||||||
|
|
||||||
direction = lib.mkOption {
|
|
||||||
description = "Direction targeted by a keybind action.";
|
|
||||||
type = lib.types.nullOr (
|
|
||||||
lib.types.enum [
|
|
||||||
"down"
|
|
||||||
"left"
|
|
||||||
"right"
|
|
||||||
"up"
|
|
||||||
]
|
|
||||||
);
|
|
||||||
default = null;
|
|
||||||
example = "left";
|
|
||||||
};
|
|
||||||
|
|
||||||
workspace = lib.mkOption {
|
|
||||||
description = "Workspace targeted by a keybind action.";
|
|
||||||
type = lib.types.nullOr lib.types.int;
|
|
||||||
default = null;
|
|
||||||
example = 1;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,7 @@ let
|
|||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# Next stable UID: 20007
|
# Next stable UID: 20005
|
||||||
|
|
||||||
type = lib.types.ints.positive;
|
type = lib.types.ints.positive;
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
{ lib, ... }:
|
|
||||||
{
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
position = lib.mkOption {
|
|
||||||
type = lib.types.enum [
|
|
||||||
"left"
|
|
||||||
"center"
|
|
||||||
"right"
|
|
||||||
];
|
|
||||||
default = "right";
|
|
||||||
description = "Selects the Waybar section that shows the module.";
|
|
||||||
};
|
|
||||||
|
|
||||||
order = lib.mkOption {
|
|
||||||
type = lib.types.int;
|
|
||||||
default = 100;
|
|
||||||
example = 50;
|
|
||||||
description = "Sorts the module within its Waybar section.";
|
|
||||||
};
|
|
||||||
|
|
||||||
settings = lib.mkOption {
|
|
||||||
type = lib.types.attrsOf lib.types.anything;
|
|
||||||
default = { };
|
|
||||||
description = "Sets the Waybar module configuration.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{ lib, ... }:
|
|
||||||
{
|
|
||||||
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
class = lib.mkOption {
|
|
||||||
description = "Matches the window class reported by the compositor.";
|
|
||||||
type = lib.types.str;
|
|
||||||
example = ".*[Ff]irefox.*";
|
|
||||||
};
|
|
||||||
|
|
||||||
icon = lib.mkOption {
|
|
||||||
description = "Sets the icon shown for matching windows.";
|
|
||||||
type = lib.types.str;
|
|
||||||
example = "";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -6,17 +6,6 @@ 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: |
|
||||||
@@ -37,7 +26,7 @@ sops:
|
|||||||
dyjzy0/9Ui1IIXH36AImO5hiEZcrGePV2qPLLQc7ZLngC1jlJuEKJA==
|
dyjzy0/9Ui1IIXH36AImO5hiEZcrGePV2qPLLQc7ZLngC1jlJuEKJA==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1wf5s0n0tgt6ld2ysgu9dc67mj8ylwecgl4utzg7hqwy3kut9zyms7aglmh
|
recipient: age1wf5s0n0tgt6ld2ysgu9dc67mj8ylwecgl4utzg7hqwy3kut9zyms7aglmh
|
||||||
lastmodified: "2026-10-20T01:27:06Z"
|
lastmodified: "2026-09-12T22:54:25Z"
|
||||||
mac: ENC[AES256_GCM,data:NCt6e/ctZwaBADfrhu1RXtILlClbHEqOAnIu8rE/zMPSwMIPB/kTtfd/ujf+1iRszoHaX4Se1xc0wz1th6NVToFVNarxBBgTk4JMUU8gdMweOjmwlh3GXU3ZcYY/xAXxWF1hxyXSEW78j3S2844H9XXENAq/Bf3mjdCYQ/qr/d4=,iv:4nFyBSmRvF4zSAwvL+xccMwtWrVbHtFKNoTb7f3rSH4=,tag:eCV/7P0TjyMZXKcWBUPXQA==,type:str]
|
mac: ENC[AES256_GCM,data:4R3a0ytf4i8aM8iBQ9vRC/q+hvtjLrCTNgbpCB5DbqBMBYmhtGsBmO/0i1yT0N4tydyLaGOMkNfuuuVi3ASLEOKnViy9w3uD9IN0F79W4f2vguWUVIp/T8skvN9wbV9o9xd0LkJzmeVVgzb1H6HKw+rTGjbquleoIHEBDcDDM+k=,iv:0NlckMXlS+idY6UGBF+F0KFLt+pzD7Q/YEQOdXraAYg=,tag:JBBQduV8uhnKz9gPf6HfAQ==,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": "visible",
|
"propertiesInDocument": "hidden",
|
||||||
"showLineNumber": true,
|
"showLineNumber": true,
|
||||||
"autoPairMarkdown": false,
|
"autoPairMarkdown": false,
|
||||||
"useTab": false,
|
"useTab": false,
|
||||||
|
|||||||
3
docs/.obsidian/community-plugins.json
vendored
3
docs/.obsidian/community-plugins.json
vendored
@@ -5,6 +5,5 @@
|
|||||||
"obsidian-style-settings",
|
"obsidian-style-settings",
|
||||||
"custom-font-loader",
|
"custom-font-loader",
|
||||||
"obsidian-icon-folder",
|
"obsidian-icon-folder",
|
||||||
"table-editor-obsidian",
|
"table-editor-obsidian"
|
||||||
"obsidian-linter"
|
|
||||||
]
|
]
|
||||||
16
docs/.obsidian/graph.json
vendored
16
docs/.obsidian/graph.json
vendored
@@ -28,20 +28,6 @@
|
|||||||
"rgb": 5366249
|
"rgb": 5366249
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"query": "path:\"03 wrk/tasks\"[\"status\":open]",
|
|
||||||
"color": {
|
|
||||||
"a": 1,
|
|
||||||
"rgb": 14725458
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"query": "path:\"03 wrk/tasks\" [\"status\":closed]",
|
|
||||||
"color": {
|
|
||||||
"a": 1,
|
|
||||||
"rgb": 11657298
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"query": "path:\"03 wrk\" ",
|
"query": "path:\"03 wrk\" ",
|
||||||
"color": {
|
"color": {
|
||||||
@@ -74,6 +60,6 @@
|
|||||||
"repelStrength": 20,
|
"repelStrength": 20,
|
||||||
"linkStrength": 1,
|
"linkStrength": 1,
|
||||||
"linkDistance": 250,
|
"linkDistance": 250,
|
||||||
"scale": 0.16202697784544698,
|
"scale": 0.3645607001522557,
|
||||||
"close": false
|
"close": false
|
||||||
}
|
}
|
||||||
326
docs/.obsidian/plugins/obsidian-linter/data.json
vendored
326
docs/.obsidian/plugins/obsidian-linter/data.json
vendored
@@ -1,326 +0,0 @@
|
|||||||
{
|
|
||||||
"ruleConfigs": {
|
|
||||||
"add-blank-line-after-yaml": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"dedupe-yaml-array-values": {
|
|
||||||
"enabled": true,
|
|
||||||
"dedupe-alias-key": true,
|
|
||||||
"dedupe-tag-key": true,
|
|
||||||
"dedupe-array-keys": true,
|
|
||||||
"ignore-keys": []
|
|
||||||
},
|
|
||||||
"escape-yaml-special-characters": {
|
|
||||||
"enabled": true,
|
|
||||||
"try-to-escape-single-line-arrays": true
|
|
||||||
},
|
|
||||||
"force-yaml-escape": {
|
|
||||||
"enabled": true,
|
|
||||||
"force-yaml-escape-keys": []
|
|
||||||
},
|
|
||||||
"format-tags-in-yaml": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"format-yaml-array": {
|
|
||||||
"enabled": true,
|
|
||||||
"alias-key": true,
|
|
||||||
"tag-key": true,
|
|
||||||
"default-array-keys": true,
|
|
||||||
"force-single-line-array-style": [],
|
|
||||||
"force-multi-line-array-style": []
|
|
||||||
},
|
|
||||||
"insert-yaml-attributes": {
|
|
||||||
"enabled": true,
|
|
||||||
"text-to-insert": []
|
|
||||||
},
|
|
||||||
"move-inline-fields-to-yaml": {
|
|
||||||
"enabled": true,
|
|
||||||
"how-to-handle-full-line-fields": "Move and remove",
|
|
||||||
"how-to-handle-bracketed-fields": "Leave in place",
|
|
||||||
"how-to-handle-existing-keys": "Skip",
|
|
||||||
"inline-keys-to-ignore": []
|
|
||||||
},
|
|
||||||
"move-tags-to-yaml": {
|
|
||||||
"enabled": true,
|
|
||||||
"how-to-handle-existing-tags": "Nothing",
|
|
||||||
"tags-to-ignore": []
|
|
||||||
},
|
|
||||||
"remove-yaml-keys": {
|
|
||||||
"enabled": true,
|
|
||||||
"yaml-keys-to-remove": []
|
|
||||||
},
|
|
||||||
"sort-yaml-array-values": {
|
|
||||||
"enabled": true,
|
|
||||||
"sort-alias-key": true,
|
|
||||||
"sort-tag-key": true,
|
|
||||||
"sort-array-keys": true,
|
|
||||||
"ignore-keys": [],
|
|
||||||
"sort-order": "Ascending Alphabetical"
|
|
||||||
},
|
|
||||||
"yaml-key-sort": {
|
|
||||||
"enabled": true,
|
|
||||||
"yaml-key-priority-sort-order": [
|
|
||||||
"aliases",
|
|
||||||
"date-created",
|
|
||||||
"date-modified"
|
|
||||||
],
|
|
||||||
"priority-keys-at-start-of-yaml": true,
|
|
||||||
"yaml-sort-order-for-other-keys": "None"
|
|
||||||
},
|
|
||||||
"yaml-timestamp": {
|
|
||||||
"enabled": true,
|
|
||||||
"date-created": true,
|
|
||||||
"date-created-key": "date-created",
|
|
||||||
"date-created-source-of-truth": "file system",
|
|
||||||
"date-modified": true,
|
|
||||||
"date-modified-key": "date-modified",
|
|
||||||
"date-modified-source-of-truth": "file system",
|
|
||||||
"format": "YYYY-MM-DD HH:mm",
|
|
||||||
"convert-to-utc": false,
|
|
||||||
"update-on-file-contents-updated": "after 5 seconds"
|
|
||||||
},
|
|
||||||
"yaml-title": {
|
|
||||||
"enabled": false,
|
|
||||||
"title-key": "title",
|
|
||||||
"mode": "first-h1-or-filename-if-h1-missing"
|
|
||||||
},
|
|
||||||
"yaml-title-alias": {
|
|
||||||
"enabled": true,
|
|
||||||
"preserve-existing-alias-section-style": true,
|
|
||||||
"keep-alias-that-matches-the-filename": false,
|
|
||||||
"use-yaml-key-to-keep-track-of-old-filename-or-heading": false,
|
|
||||||
"alias-helper-key": "linter-yaml-title-alias",
|
|
||||||
"remove-alias-if-empty": true
|
|
||||||
},
|
|
||||||
"capitalize-headings": {
|
|
||||||
"enabled": true,
|
|
||||||
"style": "Title Case",
|
|
||||||
"ignore-case-words": [],
|
|
||||||
"lowercase-words": [],
|
|
||||||
"starting-word-ignore-characters": "'\"(‘“-",
|
|
||||||
"ending-word-ignore-characters": ".?!,:;'\")”’0123456789-",
|
|
||||||
"ignore-words": []
|
|
||||||
},
|
|
||||||
"file-name-heading": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"header-increment": {
|
|
||||||
"enabled": true,
|
|
||||||
"start-at-h2": false
|
|
||||||
},
|
|
||||||
"headings-start-line": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"remove-trailing-punctuation-in-heading": {
|
|
||||||
"enabled": true,
|
|
||||||
"punctuation-to-remove": ".,;:!。,;:!"
|
|
||||||
},
|
|
||||||
"footnote-after-punctuation": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"move-footnotes-to-the-bottom": {
|
|
||||||
"enabled": true,
|
|
||||||
"include-blank-line-between-footnotes": false
|
|
||||||
},
|
|
||||||
"re-index-footnotes": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"auto-correct-common-misspellings": {
|
|
||||||
"enabled": true,
|
|
||||||
"ignore-words": [],
|
|
||||||
"skip-words-with-multiple-capitals": true,
|
|
||||||
"extra-auto-correct-files": []
|
|
||||||
},
|
|
||||||
"blockquote-style": {
|
|
||||||
"enabled": true,
|
|
||||||
"style": "space"
|
|
||||||
},
|
|
||||||
"convert-bullet-list-markers": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"default-language-for-code-fences": {
|
|
||||||
"enabled": true,
|
|
||||||
"default-language": ""
|
|
||||||
},
|
|
||||||
"emphasis-style": {
|
|
||||||
"enabled": true,
|
|
||||||
"style": "consistent"
|
|
||||||
},
|
|
||||||
"no-bare-urls": {
|
|
||||||
"enabled": true,
|
|
||||||
"no-bare-uris": true
|
|
||||||
},
|
|
||||||
"ordered-list-style": {
|
|
||||||
"enabled": true,
|
|
||||||
"number-style": "ascending",
|
|
||||||
"list-end-style": ".",
|
|
||||||
"preserve-start": false
|
|
||||||
},
|
|
||||||
"proper-ellipsis": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"quote-style": {
|
|
||||||
"enabled": true,
|
|
||||||
"single-quote-enabled": true,
|
|
||||||
"single-quote-style": "''",
|
|
||||||
"double-quote-enabled": true,
|
|
||||||
"double-quote-style": "\"\""
|
|
||||||
},
|
|
||||||
"remove-consecutive-list-markers": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"remove-empty-list-markers": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"remove-hyphenated-line-breaks": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"remove-multiple-spaces": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"strong-style": {
|
|
||||||
"enabled": true,
|
|
||||||
"style": "consistent"
|
|
||||||
},
|
|
||||||
"two-spaces-between-lines-with-content": {
|
|
||||||
"enabled": false,
|
|
||||||
"line-break-indicator": " "
|
|
||||||
},
|
|
||||||
"unordered-list-style": {
|
|
||||||
"enabled": true,
|
|
||||||
"list-style": "consistent"
|
|
||||||
},
|
|
||||||
"align-table-columns": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"compact-yaml": {
|
|
||||||
"enabled": true,
|
|
||||||
"inner-new-lines": false
|
|
||||||
},
|
|
||||||
"consecutive-blank-lines": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"convert-spaces-to-tabs": {
|
|
||||||
"enabled": true,
|
|
||||||
"tabsize": 4
|
|
||||||
},
|
|
||||||
"empty-line-around-blockquotes": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"empty-line-around-code-fences": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"empty-line-around-horizontal-rules": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"empty-line-around-math-blocks": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"empty-line-around-tables": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"heading-blank-lines": {
|
|
||||||
"enabled": true,
|
|
||||||
"bottom": true,
|
|
||||||
"empty-line-after-yaml": true
|
|
||||||
},
|
|
||||||
"line-break-at-document-end": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"move-math-block-indicators-to-their-own-line": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"paragraph-blank-lines": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"remove-empty-lines-between-list-markers-and-checklists": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"remove-link-spacing": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"remove-space-around-characters": {
|
|
||||||
"enabled": true,
|
|
||||||
"include-fullwidth-forms": true,
|
|
||||||
"include-cjk-symbols-and-punctuation": true,
|
|
||||||
"include-dashes": true,
|
|
||||||
"other-symbols": ""
|
|
||||||
},
|
|
||||||
"remove-space-before-or-after-characters": {
|
|
||||||
"enabled": true,
|
|
||||||
"characters-to-remove-space-before": ",!?;:).’”]",
|
|
||||||
"characters-to-remove-space-after": "¿¡‘“(["
|
|
||||||
},
|
|
||||||
"space-after-list-markers": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"space-between-chinese-japanese-or-korean-and-english-or-numbers": {
|
|
||||||
"enabled": true,
|
|
||||||
"english-symbols-punctuation-before": "-+;:'\"°%$)]",
|
|
||||||
"english-symbols-punctuation-after": "-+'\"([¥$"
|
|
||||||
},
|
|
||||||
"trailing-spaces": {
|
|
||||||
"enabled": true,
|
|
||||||
"two-space-line-break": false
|
|
||||||
},
|
|
||||||
"add-blockquote-indentation-on-paste": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"prevent-double-checklist-indicator-on-paste": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"prevent-double-list-item-indicator-on-paste": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"proper-ellipsis-on-paste": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"remove-hyphens-on-paste": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"remove-leading-or-trailing-whitespace-on-paste": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"remove-leftover-footnotes-from-quote-on-paste": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"remove-multiple-blank-lines-on-paste": {
|
|
||||||
"enabled": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lintOnSave": true,
|
|
||||||
"recordLintOnSaveLogs": false,
|
|
||||||
"displayChanged": true,
|
|
||||||
"suppressMessageWhenNoChange": true,
|
|
||||||
"enableDiffPreviewView": true,
|
|
||||||
"suppressLintAllFilesConfirmationModal": false,
|
|
||||||
"suppressLintAllFilesInFolderConfirmationModal": false,
|
|
||||||
"lintOnFileChange": false,
|
|
||||||
"displayLintOnFileChangeNotice": false,
|
|
||||||
"settingsConvertedToConfigKeyValues": true,
|
|
||||||
"textAreaSettingsConvertedToListItemSettings": true,
|
|
||||||
"defaultArrayStyleMovedToCommonStyles": true,
|
|
||||||
"additionalFileExtensions": [],
|
|
||||||
"foldersToIgnore": [
|
|
||||||
"99 meta/templates"
|
|
||||||
],
|
|
||||||
"filesToIgnore": [
|
|
||||||
{
|
|
||||||
"label": "todo",
|
|
||||||
"match": "TODO.md",
|
|
||||||
"flags": "i"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"linterLocale": "system-default",
|
|
||||||
"logLevel": "ERROR",
|
|
||||||
"lintCommands": [],
|
|
||||||
"customRegexes": [],
|
|
||||||
"commonStyles": {
|
|
||||||
"aliasArrayStyle": "multi-line",
|
|
||||||
"tagArrayStyle": "single-line",
|
|
||||||
"defaultArrayStyle": "multi-line",
|
|
||||||
"minimumNumberOfDollarSignsToBeAMathBlock": 2,
|
|
||||||
"escapeCharacter": "\"",
|
|
||||||
"removeUnnecessaryEscapeCharsForMultiLineArrays": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
399
docs/.obsidian/plugins/obsidian-linter/main.js
vendored
399
docs/.obsidian/plugins/obsidian-linter/main.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "obsidian-linter",
|
|
||||||
"name": "Linter",
|
|
||||||
"version": "1.34.1",
|
|
||||||
"minAppVersion": "1.13.0",
|
|
||||||
"description": "Formats and styles your notes. It can be used to format YAML tags, aliases, arrays, and metadata; footnotes; headings; spacing; math blocks; regular markdown contents like list, italics, and bold styles; and more with the use of custom rule options as well.",
|
|
||||||
"author": "Victor Tao",
|
|
||||||
"authorUrl": "https://github.com/platers",
|
|
||||||
"helpUrl": "https://platers.github.io/obsidian-linter/",
|
|
||||||
"isDesktopOnly": false
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.linter-no-border{border:none}.linter-border-bottom{border-bottom:1px solid var(--background-modifier-border);border-top:0;margin-bottom:.75em}.custom-row-description{margin-top:0}.modal-warn,.search-zero-state{font-weight:700}.modal-heading,.search-zero-state{text-align:center}.confirm-modal-checkbox-container{align-items:center;display:flex;gap:6px;margin-top:8px}.disabled-list-entry{color:var(--text-faint)}.linter-diff-preview-view{display:flex;flex-direction:column;height:100%;min-width:0;overflow:hidden}.linter-diff-preview-empty{color:var(--text-muted);padding:16px}.linter-diff-preview-header{align-items:flex-start;border-bottom:1px solid var(--background-modifier-border);display:flex;flex:0 0 auto;gap:12px;justify-content:space-between;padding:12px 14px}.linter-diff-preview-title-group{min-width:0}.linter-diff-preview-header h3{font-size:var(--font-ui-medium);line-height:1.3;margin:0 0 6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.linter-diff-preview-actions{display:flex;flex:0 0 auto;gap:8px}.linter-diff-preview-body{flex:1 1 auto;min-height:0;overflow:auto;padding:12px 14px 16px}.linter-diff-preview-view .linter-diff-summary{color:var(--text-normal);display:inline-block;font-size:var(--font-ui-small);margin:0 0 10px;padding:5px 8px}.linter-diff-preview-view .linter-diff,.linter-diff-preview-view .linter-diff-summary{background:var(--background-secondary);border:1px solid var(--background-modifier-border);border-radius:6px}.linter-diff-preview-view .linter-diff{font-family:var(--font-monospace);font-size:var(--font-smaller);line-height:1.45;margin:0;overflow:hidden;padding:6px 0}.linter-diff-preview-view .linter-diff-line{display:grid;grid-template-columns:26px minmax(0,1fr);min-height:20px;padding:1px 10px 1px 0}.linter-diff-preview-view .linter-diff-marker{color:var(--text-muted);padding-right:8px;text-align:right;user-select:none}.linter-diff-preview-view .linter-diff-text{min-width:0;overflow-wrap:anywhere;white-space:pre-wrap}.linter-diff-preview-view .linter-diff-skip{border-bottom:1px solid var(--background-modifier-border);border-top:1px solid var(--background-modifier-border);color:var(--text-faint);font-family:var(--font-interface);font-size:var(--font-ui-smaller);padding:4px 12px;text-align:center}.linter-diff-preview-view .linter-diff-added{background:rgba(var(--color-green-rgb),.12);color:var(--text-normal)}.linter-diff-preview-view .linter-diff-removed{background:rgba(var(--color-red-rgb),.12);color:var(--text-normal)}.linter-diff-preview-view .linter-diff-context{color:var(--text-muted)}
|
|
||||||
78
docs/.obsidian/workspace.json
vendored
78
docs/.obsidian/workspace.json
vendored
@@ -4,21 +4,21 @@
|
|||||||
"type": "split",
|
"type": "split",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "81c4860f495113bf",
|
"id": "6d9088d392cb8604",
|
||||||
"type": "tabs",
|
"type": "tabs",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "daab69fc3faab88f",
|
"id": "22a4c1f201a1b00e",
|
||||||
"type": "leaf",
|
"type": "leaf",
|
||||||
"state": {
|
"state": {
|
||||||
"type": "markdown",
|
"type": "markdown",
|
||||||
"state": {
|
"state": {
|
||||||
"file": "00 INBOX/credits.md",
|
"file": "TODO.md",
|
||||||
"mode": "source",
|
"mode": "source",
|
||||||
"source": true
|
"source": true
|
||||||
},
|
},
|
||||||
"icon": "lucide-file",
|
"icon": "lucide-file",
|
||||||
"title": "credits"
|
"title": "TODO"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"type": "localgraph",
|
"type": "localgraph",
|
||||||
"state": {
|
"state": {
|
||||||
"file": "00 INBOX/credits.md",
|
"file": "TODO.md",
|
||||||
"options": {
|
"options": {
|
||||||
"collapse-filter": true,
|
"collapse-filter": true,
|
||||||
"search": "",
|
"search": "",
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
"linkStrength": 1,
|
"linkStrength": 1,
|
||||||
"linkDistance": 250,
|
"linkDistance": 250,
|
||||||
"scale": 0.6666666666666666,
|
"scale": 0.6666666666666666,
|
||||||
"close": false
|
"close": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"icon": "lucide-git-fork",
|
"icon": "lucide-git-fork",
|
||||||
@@ -184,6 +184,20 @@
|
|||||||
"title": "tag"
|
"title": "tag"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "8918bf52a32e4a8e",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "all-properties",
|
||||||
|
"state": {
|
||||||
|
"sortOrder": "frequency",
|
||||||
|
"showSearch": false,
|
||||||
|
"searchQuery": ""
|
||||||
|
},
|
||||||
|
"icon": "lucide-ghost",
|
||||||
|
"title": "all-properties"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "c10908b3bd97f5fa",
|
"id": "c10908b3bd97f5fa",
|
||||||
"type": "leaf",
|
"type": "leaf",
|
||||||
@@ -217,43 +231,43 @@
|
|||||||
"table-editor-obsidian:Advanced Tables Toolbar": false
|
"table-editor-obsidian:Advanced Tables Toolbar": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"active": "daab69fc3faab88f",
|
"active": "22a4c1f201a1b00e",
|
||||||
"lastOpenFiles": [
|
"lastOpenFiles": [
|
||||||
"00 INBOX/ai/tldr-design.md",
|
|
||||||
"00 INBOX/ai/caps-escape-design.md",
|
|
||||||
"AGENTS.md",
|
|
||||||
"00 INBOX/ai/tldr-design-tree.md",
|
|
||||||
"00 INBOX/ai/grill-skill-design.md",
|
|
||||||
"03 wrk/tasks/0012-gitea-axi-pr-merge-guardrail.md",
|
|
||||||
"03 wrk/tasks/0011-gitea-axi-rust-migration.md",
|
|
||||||
"00 INBOX/ai/gitea-axi-pr-merge-guardrail.md",
|
|
||||||
"00 INBOX/ai/gitea-axi-rust-migration.md",
|
|
||||||
"00 INBOX/valheim progression.md",
|
"00 INBOX/valheim progression.md",
|
||||||
"03 wrk/tasks/0010-fix-main-monitor-detection.md",
|
|
||||||
"00 INBOX/credits.md",
|
|
||||||
"03 wrk/tasks/0009-bazel-project-scaffolding.md",
|
|
||||||
"03 wrk/tasks/0005-rust-cli-scaffolding.md",
|
|
||||||
"03 wrk/tasks/0008-vim-obsidian-markdown-renderer.md",
|
|
||||||
"03 wrk/tasks/0002-nixos-refactor.md",
|
|
||||||
"TODO.md",
|
"TODO.md",
|
||||||
"technical-writing.md",
|
"technical-writing.md",
|
||||||
"dictionary.md",
|
"rust-module-prototype.md",
|
||||||
"03 wrk/tasks/0007-obsidian-source-notes.md",
|
"00 INBOX/ai/statix-architecture.md",
|
||||||
"03 wrk/tasks/0004-dictionary-cli.md",
|
"00 INBOX/ai/nix-statix-deadnix-rules.md",
|
||||||
"03 wrk/tasks/0006-wallpaper-randomizer.md",
|
|
||||||
"03 wrk/tasks/0003-whisper-dictation.md",
|
|
||||||
"03 wrk/monorepo.md",
|
|
||||||
"03 wrk/tasks/0001-todo-cli.md",
|
|
||||||
"03 wrk/productivity.md",
|
|
||||||
"03 wrk/tasks",
|
|
||||||
"00 INBOX/let order name resolution.md~",
|
"00 INBOX/let order name resolution.md~",
|
||||||
|
"00 INBOX/nix name resolution.md",
|
||||||
|
"00 INBOX/let order name resolution.md",
|
||||||
"00 INBOX/nix name resolution.md~",
|
"00 INBOX/nix name resolution.md~",
|
||||||
|
"00 INBOX/ai/deadnix-architecture.md",
|
||||||
"TODO.md~",
|
"TODO.md~",
|
||||||
|
"00 INBOX/ai/xatu-rule-implementation-order.md",
|
||||||
|
"00 INBOX/ai/xatu-architecture-design.md",
|
||||||
|
"00 INBOX/ai/nix-style-linter-plan.md",
|
||||||
|
"00 INBOX/ai/nix-style-guide-agent-customization.md",
|
||||||
|
"00 INBOX/ai/game-guest-readiness-plan.md",
|
||||||
|
"00 INBOX/ai/xatu-node-rule-static-dispatch-plan.md",
|
||||||
|
"00 INBOX/ai/nix-style-linter-pr-1-vendor-rnix.md",
|
||||||
|
"00 INBOX/ai/nix-style-linter-pr-3-let-order.md",
|
||||||
|
"00 INBOX/ai/nix-style-linter-pr-2-scaffold-cli.md",
|
||||||
|
"00 INBOX/ai/homepage-pikachu-setup-plan.md",
|
||||||
|
"00 INBOX/ai/source-driven-remote-deploy-plan.md",
|
||||||
|
"00 INBOX/ai/monorepo-work-report.md",
|
||||||
|
"00 INBOX/ai/clew-ast-exploration.md",
|
||||||
|
"00 INBOX/ai/steamcmd servers.md",
|
||||||
|
"00 INBOX/ai/ariadne-pipeline-stage-boundary.md",
|
||||||
|
"00 INBOX/ai/ariadne-project-goals.md",
|
||||||
|
"02 notes/steamcmd server setup.md",
|
||||||
"00 INBOX/ai/clew-ast-exploration.md~",
|
"00 INBOX/ai/clew-ast-exploration.md~",
|
||||||
"03 wrk/machoke.md~",
|
"03 wrk/machoke.md~",
|
||||||
"03 wrk/falinks.md~",
|
"03 wrk/falinks.md~",
|
||||||
"99 meta/attachments/bases/unlinked mentions.base",
|
"99 meta/attachments/bases/unlinked mentions.base",
|
||||||
"00 INBOX/ai",
|
"00 INBOX/ai",
|
||||||
"03 wrk/ai"
|
"03 wrk/ai",
|
||||||
|
"99 meta/Untitled"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# Caps Escape Design
|
|
||||||
|
|
||||||
## Facts
|
|
||||||
|
|
||||||
- Commit `250b0b71f` removed `services.xserver.xkb.options` from the shared host module.
|
|
||||||
- The removed option was `caps:escape_shifted_capslock`.
|
|
||||||
- That XKB option maps Caps Lock to Escape.
|
|
||||||
- Shift+Caps Lock still enables Caps Lock.
|
|
||||||
- The shared host module configures Neogaia, Zeus, and Pikachu.
|
|
||||||
- Zeus has QMK firmware that provides equivalent keyboard behavior.
|
|
||||||
- Neogaia and Zeus enable `modules.host.cachyos` and `modules.desktop`.
|
|
||||||
- Pikachu enables neither module.
|
|
||||||
- `configs/nixos/hosts/neogaia/default.nix` directly sets Neogaia-specific hardware, networking, desktop application, and Waybar settings.
|
|
||||||
- The current configuration has no Caps Lock or Escape remapping.
|
|
||||||
- This session runs on Zeus, not Neogaia.
|
|
||||||
|
|
||||||
## Design Tree
|
|
||||||
|
|
||||||
- Restore Caps Lock behavior
|
|
||||||
- Scope the configuration
|
|
||||||
- Configure Neogaia only. ✓
|
|
||||||
- Select the mapping behavior.
|
|
||||||
- Restore `caps:escape_shifted_capslock`. ✓
|
|
||||||
- Set `services.xserver.xkb.options` in `configs/nixos/hosts/neogaia/default.nix`. ✓
|
|
||||||
- Define validation for Neogaia.
|
|
||||||
- Run static checks and manually verify both keys after deployment. ✓
|
|
||||||
- Stop after source and static validation. ✓
|
|
||||||
|
|
||||||
## Settled Decisions
|
|
||||||
|
|
||||||
1. Configure the mapping on Neogaia only.
|
|
||||||
2. Restore `caps:escape_shifted_capslock`.
|
|
||||||
3. Set `services.xserver.xkb.options` directly in Neogaia's host configuration.
|
|
||||||
4. Run static checks and manually verify both keys on Neogaia after deployment.
|
|
||||||
5. Stop after source and static validation.
|
|
||||||
|
|
||||||
## Frontier
|
|
||||||
|
|
||||||
None.
|
|
||||||
304
docs/00 INBOX/ai/deadnix-architecture.md
Normal file
304
docs/00 INBOX/ai/deadnix-architecture.md
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
# Deadnix Architecture
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This note explains how `deadnix` finds unused Nix code.
|
||||||
|
It focuses on architecture, not whether each check belongs in our linter.
|
||||||
|
|
||||||
|
Use this note when deciding whether our linter needs scope analysis.
|
||||||
|
|
||||||
|
## What Deadnix Is
|
||||||
|
|
||||||
|
`deadnix` is a dead-code scanner for Nix.
|
||||||
|
It finds bindings that introduce names which no live code uses.
|
||||||
|
|
||||||
|
It is not a general style linter.
|
||||||
|
It does not have many independent rules.
|
||||||
|
It implements one analysis family: unused declarations.
|
||||||
|
|
||||||
|
Like `statix`, it parses source text with `rnix`.
|
||||||
|
It does not evaluate Nix.
|
||||||
|
It does not import files.
|
||||||
|
It reasons over local syntax and local scope only.
|
||||||
|
|
||||||
|
## Minimal Dead-Code Vocabulary
|
||||||
|
|
||||||
|
- A binding introduces a name.
|
||||||
|
- A scope defines where introduced names are visible.
|
||||||
|
- A use is an identifier occurrence that refers to a binding.
|
||||||
|
- Shadowing happens when an inner scope reuses an outer name.
|
||||||
|
- Dead code is a binding that no live code uses.
|
||||||
|
- A fixed point is the final result after repeated analysis no longer changes.
|
||||||
|
|
||||||
|
Dead-code analysis needs more state than a simple syntax lint.
|
||||||
|
It must know which names a scope introduces and where those names can be used.
|
||||||
|
|
||||||
|
## Component Map
|
||||||
|
|
||||||
|
`deadnix` has fewer layers than `statix`.
|
||||||
|
The command-line entry point, file walk, parsing, analysis, reporting, and editing live in one small tool.
|
||||||
|
|
||||||
|
The important concepts are separate modules:
|
||||||
|
|
||||||
|
- settings control analysis options;
|
||||||
|
- scopes describe where names are introduced;
|
||||||
|
- bindings describe individual declared names;
|
||||||
|
- usage search answers whether a name appears in a live body;
|
||||||
|
- dead-code results connect a binding to its scope;
|
||||||
|
- reports render findings;
|
||||||
|
- edits remove or rename unused declarations.
|
||||||
|
|
||||||
|
## Main Pipeline
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[Parse CLI options] --> B[Collect input files]
|
||||||
|
B --> C[Read each file]
|
||||||
|
C --> D[Parse with rnix]
|
||||||
|
D --> E[Skip file on parse errors]
|
||||||
|
D --> F[Run dead-code analysis]
|
||||||
|
F --> G[Print reports unless quiet]
|
||||||
|
G --> H[Apply edits when --edit is set]
|
||||||
|
H --> I[Exit 1 when --fail and findings exist]
|
||||||
|
```
|
||||||
|
|
||||||
|
The CLI accepts files or directories.
|
||||||
|
Directory mode walks `.nix` files.
|
||||||
|
Hidden files require an explicit option.
|
||||||
|
Excludes filter paths before analysis.
|
||||||
|
|
||||||
|
The tool skips dead-code diagnostics when a file has parse errors.
|
||||||
|
It reports parse errors to the user and continues with other files.
|
||||||
|
|
||||||
|
## Analysis Settings
|
||||||
|
|
||||||
|
The analysis has four important switches.
|
||||||
|
|
||||||
|
`no_lambda_arg` disables checks for single lambda arguments.
|
||||||
|
This can avoid noise when an API requires an argument name.
|
||||||
|
|
||||||
|
`no_lambda_pattern_names` disables checks for names in attribute-set function patterns.
|
||||||
|
This matters for `callPackage`-style code, where argument names can be external API.
|
||||||
|
|
||||||
|
`no_underscore` ignores bindings that start with `_`.
|
||||||
|
This treats `_name` as an intentional unused placeholder.
|
||||||
|
|
||||||
|
`warn_used_underscore` reports underscore-prefixed bindings that code actually uses.
|
||||||
|
This enforces the convention that `_name` means unused.
|
||||||
|
|
||||||
|
These settings are architecture-relevant because dead-code policy depends on API context.
|
||||||
|
A repository linter needs a way to express that context.
|
||||||
|
|
||||||
|
## Scope Model
|
||||||
|
|
||||||
|
`deadnix` recognizes four scope types.
|
||||||
|
|
||||||
|
### Lambda pattern scope
|
||||||
|
|
||||||
|
A lambda pattern is a function argument written as an attribute set pattern.
|
||||||
|
It can introduce field names and an optional alias for the whole argument set.
|
||||||
|
|
||||||
|
Example shape:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
args @ { pkgs, lib, ... }: body
|
||||||
|
```
|
||||||
|
|
||||||
|
This scope can introduce `args`, `pkgs`, and `lib`.
|
||||||
|
|
||||||
|
### Lambda argument scope
|
||||||
|
|
||||||
|
A single lambda argument introduces one name.
|
||||||
|
|
||||||
|
Example shape:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
pkgs: body
|
||||||
|
```
|
||||||
|
|
||||||
|
This scope introduces `pkgs`.
|
||||||
|
|
||||||
|
### Let scope
|
||||||
|
|
||||||
|
A `let` expression introduces bindings and inherited names.
|
||||||
|
Those names are available in binding expressions and in the body.
|
||||||
|
|
||||||
|
Example shape:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
let
|
||||||
|
inherit (pkgs) hello;
|
||||||
|
value = hello;
|
||||||
|
in
|
||||||
|
value
|
||||||
|
```
|
||||||
|
|
||||||
|
This scope introduces `hello` and `value`.
|
||||||
|
|
||||||
|
### Recursive attribute-set scope
|
||||||
|
|
||||||
|
A `rec` attribute set introduces names that sibling attributes can reference.
|
||||||
|
`deadnix` models this scope, but treats its bindings as non-removable.
|
||||||
|
|
||||||
|
That conservative choice avoids deleting public-looking attributes from recursive sets.
|
||||||
|
|
||||||
|
## Binding Model
|
||||||
|
|
||||||
|
A binding stores three facts.
|
||||||
|
|
||||||
|
It stores the identifier name.
|
||||||
|
It stores the syntax node that declared the name.
|
||||||
|
It stores whether the binding is mortal.
|
||||||
|
|
||||||
|
A mortal binding can become a dead-code finding.
|
||||||
|
A non-mortal binding participates in name lookup but will not be removed.
|
||||||
|
|
||||||
|
Single lambda arguments that already start with `_` are non-mortal.
|
||||||
|
Recursive attribute-set bindings are also non-mortal.
|
||||||
|
|
||||||
|
A binding also checks for the `deadnix: skip` pragma.
|
||||||
|
The pragma applies to a nearby following declaration.
|
||||||
|
The scanner searches backward from the declaration and stops after a blank-line boundary.
|
||||||
|
|
||||||
|
## Usage Search
|
||||||
|
|
||||||
|
Usage search asks whether a given name appears inside a syntax subtree.
|
||||||
|
|
||||||
|
The search is scope-aware.
|
||||||
|
When it enters a child scope that binds the same name, it stops following that branch.
|
||||||
|
That prevents an inner binding from counting as a use of an outer binding.
|
||||||
|
|
||||||
|
Attribute paths get special handling.
|
||||||
|
Ordinary attribute keys do not count as uses.
|
||||||
|
Dynamic or string attribute names can contain expressions, so the search looks inside those parts.
|
||||||
|
|
||||||
|
`inherit` also gets special handling.
|
||||||
|
A scope can inherit names from an outer scope.
|
||||||
|
The analysis checks whether the inherited expression uses the searched name.
|
||||||
|
|
||||||
|
## Fixed-Point Analysis
|
||||||
|
|
||||||
|
`deadnix` repeats the scan until the result set stops changing.
|
||||||
|
|
||||||
|
This matters because dead code can depend on other dead code.
|
||||||
|
A binding can appear used during the first pass because another binding references it.
|
||||||
|
If the referring binding is dead, the referenced binding is also dead.
|
||||||
|
|
||||||
|
The loop tracks dead declaration nodes.
|
||||||
|
During later scans, uses inside already-dead bodies do not keep other bindings alive.
|
||||||
|
|
||||||
|
This fixed-point loop is the central architectural difference from simple local syntax lints.
|
||||||
|
It makes dead-code results transitive.
|
||||||
|
|
||||||
|
## Reporting Model
|
||||||
|
|
||||||
|
A result stores the scope and binding for one finding.
|
||||||
|
The display text combines both parts.
|
||||||
|
Examples include unused lambda pattern, unused lambda argument, and unused let binding.
|
||||||
|
|
||||||
|
Human output uses `ariadne`.
|
||||||
|
The reporter groups ranges by line and labels the exact binding names.
|
||||||
|
It uses different colors for different scope kinds.
|
||||||
|
|
||||||
|
JSON output exists behind a feature flag.
|
||||||
|
It emits file, message, line, column, and end column.
|
||||||
|
|
||||||
|
`NO_COLOR` disables colored human output.
|
||||||
|
|
||||||
|
## Edit Model
|
||||||
|
|
||||||
|
`deadnix --edit` converts findings into text edits.
|
||||||
|
Each edit has a start offset, end offset, and replacement string.
|
||||||
|
|
||||||
|
The edit behavior depends on scope kind.
|
||||||
|
|
||||||
|
For unused lambda pattern names, it removes the name and nearby comma or whitespace.
|
||||||
|
For unused pattern aliases, it removes the alias syntax.
|
||||||
|
For unused lambda arguments, it renames the argument with an underscore prefix.
|
||||||
|
For unused `let` assignments, it deletes the assignment.
|
||||||
|
For unused inherited names, it deletes only the inherited name.
|
||||||
|
|
||||||
|
After those edits, it parses the edited source again.
|
||||||
|
Then it removes empty `let in` constructs and empty `inherit` statements.
|
||||||
|
|
||||||
|
This cleanup pass prevents the first edit batch from leaving invalid or useless scaffolding.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
If a path cannot be stated or read, the CLI reports the file error and continues.
|
||||||
|
If a file has parse errors, the CLI reports those errors and skips dead-code analysis for that file.
|
||||||
|
|
||||||
|
The `--fail` flag changes exit status when findings exist.
|
||||||
|
Without `--fail`, findings do not make the command fail.
|
||||||
|
|
||||||
|
This design separates report generation from CI policy.
|
||||||
|
|
||||||
|
## Architectural Strengths
|
||||||
|
|
||||||
|
The scope model is explicit.
|
||||||
|
Each scope type declares which bindings it introduces and where those bindings are visible.
|
||||||
|
|
||||||
|
The fixed-point loop catches transitive dead code.
|
||||||
|
A binding used only by dead code also becomes dead.
|
||||||
|
|
||||||
|
The usage search handles shadowing.
|
||||||
|
That prevents common false positives in nested functions and nested `let` expressions.
|
||||||
|
|
||||||
|
The edit model is tied to analysis results.
|
||||||
|
It knows whether a binding came from a lambda, pattern, `let`, or `inherit`.
|
||||||
|
|
||||||
|
The tool has few moving parts.
|
||||||
|
That makes the analysis easy to follow compared to a general multi-rule linter.
|
||||||
|
|
||||||
|
## Architectural Limits
|
||||||
|
|
||||||
|
`deadnix` does not evaluate Nix.
|
||||||
|
It cannot know which names external callers depend on.
|
||||||
|
This matters for module arguments, package function arguments, and public attribute sets.
|
||||||
|
|
||||||
|
It has global switches rather than project-specific rule contexts.
|
||||||
|
For example, all lambda pattern names can be checked or skipped.
|
||||||
|
The tool does not know that one directory contains `callPackage` functions.
|
||||||
|
|
||||||
|
The analysis is local to one file.
|
||||||
|
It cannot see that another file imports a function and relies on its argument names.
|
||||||
|
|
||||||
|
The fixer performs text edits with byte ranges.
|
||||||
|
It must handle punctuation and whitespace carefully.
|
||||||
|
Pattern edits are harder than simple assignment deletion.
|
||||||
|
|
||||||
|
The tool has one analysis family.
|
||||||
|
It is not a framework for many unrelated rules.
|
||||||
|
|
||||||
|
## Lessons for Our Linter
|
||||||
|
|
||||||
|
Dead-code checks need a scope pass.
|
||||||
|
A per-node rule interface is not enough when a diagnostic depends on name visibility.
|
||||||
|
|
||||||
|
Represent scopes explicitly.
|
||||||
|
Each scope type should provide introduced bindings and visible bodies.
|
||||||
|
|
||||||
|
Track shadowing before reporting unused names.
|
||||||
|
A text search for an identifier is not sufficient.
|
||||||
|
|
||||||
|
Use fixed-point analysis for transitive dead code.
|
||||||
|
One pass misses names used only by dead declarations.
|
||||||
|
|
||||||
|
Separate policy from mechanics.
|
||||||
|
The mechanics can find unused names.
|
||||||
|
Repository policy decides whether module arguments, package arguments, or underscore names count.
|
||||||
|
|
||||||
|
Treat autofix as scope-specific.
|
||||||
|
Removing a pattern name is not the same operation as deleting a `let` assignment.
|
||||||
|
|
||||||
|
Run cleanup after edits.
|
||||||
|
Dead-code fixes can create empty `let` expressions and empty `inherit` statements.
|
||||||
|
|
||||||
|
## Open Design Questions for Our Linter
|
||||||
|
|
||||||
|
- Should dead-code analysis be part of `nix-style-linter`, or a separate command?
|
||||||
|
- Should policy vary by path, file kind, or suppression comment?
|
||||||
|
- Should recursive attribute-set members ever become removable findings?
|
||||||
|
- Should underscore names mean ignored, or should used underscore names be errors?
|
||||||
|
- Should the linter report unused names without offering autofix first?
|
||||||
|
- Should cleanup rules run as ordinary rules or as a post-fix pass?
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
# NixOS AGENTS and style-skill scope design tree
|
|
||||||
|
|
||||||
This tree reviews `configs/nixos/AGENTS.md`.
|
|
||||||
It decides which instructions belong in Nix style rules.
|
|
||||||
|
|
||||||
## Known facts
|
|
||||||
|
|
||||||
- `configs/nixos/AGENTS.md` has 70 lines.
|
|
||||||
- Each frontier question quotes the complete instruction under review.
|
|
||||||
- This review decides instruction placement only.
|
|
||||||
- It supplies guidance for desktop, Firefox, Neovim, AI tooling, and NixOS gotchas.
|
|
||||||
- The `/review` prompt loads `nix-style` for Nix files.
|
|
||||||
- `nix-style` loads every rule under `src/skills/style/nix/rules/`.
|
|
||||||
- The current Nix rules cover Nix syntax and flake structure.
|
|
||||||
- `configs/nixos/AGENTS.md` includes a guest-module ownership rule.
|
|
||||||
- The RomM session missed that ownership rule during implementation.
|
|
||||||
- `registry.window-icon` exists in `configs/nixos/registry/window-icon.nix`.
|
|
||||||
- Desktop application modules declare window classes and rendered icons in that registry.
|
|
||||||
- Hyprland reads the registry to generate workspace icon mappings.
|
|
||||||
- The AGENTS instruction named the registry after its introduction on 2026-10-11.
|
|
||||||
- The `class` option describes its window-class match.
|
|
||||||
- The `icon` option describes a string icon value.
|
|
||||||
- The registry schema does not document the rendered-string requirement or the class-discovery command.
|
|
||||||
- Hyprland renders `config.registry.env` as its `env` configuration.
|
|
||||||
- The `env` registry replaced direct Hyprland environment declarations in commit `370cab8f8`.
|
|
||||||
- `configs/nixos/modules/desktop/apps/r2modman.nix` enables a graphical application without a `registry.window-icon` declaration.
|
|
||||||
- The current registry modules already validate value uniqueness and field combinations.
|
|
||||||
- They do not validate required producer declarations.
|
|
||||||
- The registry collector is specific to the NixOS dotfiles configuration.
|
|
||||||
- A non-discoverable environment fact records local behavior that configuration, source code, and normal tool help do not reveal.
|
|
||||||
- The current instruction groups are:
|
|
||||||
- Desktop: window icons, Hyprland environment variables, and cursor settings.
|
|
||||||
- Firefox: search-engine declarations and extension-storage compatibility.
|
|
||||||
- Neovim: Nixvim package-set selection.
|
|
||||||
- AI tooling: Herdr key names and Pi configuration locations.
|
|
||||||
- Gotchas: registry APIs, guest boundaries, SOPS ownership, guest mounts, deployment, DNS, hardware, module paths, and host commands.
|
|
||||||
|
|
||||||
## Decision tree
|
|
||||||
|
|
||||||
- Instruction review
|
|
||||||
- Settled: Review each complete instruction in source order.
|
|
||||||
- Registry conventions
|
|
||||||
- Settled: Create `src/skills/style/nix/rules/dotfiles.md`.
|
|
||||||
- Graphical application icon instruction
|
|
||||||
- Settled: Move the complete instruction to `src/skills/style/nix/rules/dotfiles.md`.
|
|
||||||
- Hyprland session environment instruction
|
|
||||||
- Settled: Remove it because `registry.env` superseded it.
|
|
||||||
- Firefox search-engine instruction
|
|
||||||
- Settled: Remove it because the Firefox module documents the configuration.
|
|
||||||
- Firefox extension-storage instruction
|
|
||||||
- Settled: Move it to a comment above `extensions.settings` in `modules/desktop/apps/firefox.nix`.
|
|
||||||
- Neovim package-set instruction
|
|
||||||
- Settled: Move it to a comment above `nixpkgs.useGlobalPackages` in `modules/shell/nvim/default.nix`.
|
|
||||||
- Herdr key-name instruction
|
|
||||||
- Settled: Remove it.
|
|
||||||
- Pi extension location instruction
|
|
||||||
- Settled: Remove it. It recorded a completed migration from NixOS-managed sources.
|
|
||||||
- Pi generated-settings instruction
|
|
||||||
- Settled: Remove it because the Pi module documents the managed boundary.
|
|
||||||
- Library-function qualification instruction
|
|
||||||
- Settled: Generalize it and move it to `src/skills/style/nix/rules/general.md`.
|
|
||||||
- Guest ownership instruction
|
|
||||||
- Settled: Move it to `src/skills/style/nix/rules/dotfiles.md`.
|
|
||||||
- SOPS guest-owner instruction
|
|
||||||
- Settled: Move it to `src/skills/style/nix/rules/dotfiles.md`.
|
|
||||||
- Nested guest-mount instruction
|
|
||||||
- Settled: Remove it because the example describes no active layout.
|
|
||||||
- `deploy-to` instruction
|
|
||||||
- Settled: Remove it. Agents cannot complete its remote sudo steps.
|
|
||||||
- Pikachu deploy-closure instruction
|
|
||||||
- Settled: Remove it because `deploy-to` encodes the required import path.
|
|
||||||
- Public-DNS instruction
|
|
||||||
- Settled: Remove it.
|
|
||||||
- Hardware-configuration instruction
|
|
||||||
- Settled: Remove it because the root auto-generated-file rule owns the policy.
|
|
||||||
- Chaotic-cache instruction
|
|
||||||
- Settled: Remove it because `modules.host.cachyos.enable` owns the cache configuration.
|
|
||||||
- Module-relative-path instruction
|
|
||||||
- Settled: Remove it because Nix evaluation reports invalid paths.
|
|
||||||
- Pikachu `uptime` instruction
|
|
||||||
- Settled: Remove it because the command exposes its supported options.
|
|
||||||
- Guest-ACME-evaluation instruction
|
|
||||||
- Settled: Remove it because current direct guest evaluation succeeds.
|
|
||||||
- systemd executable instruction
|
|
||||||
- Settled: Remove it because `general.md` already requires `lib.getExe'` for non-main executables.
|
|
||||||
|
|
||||||
## Frontier
|
|
||||||
|
|
||||||
The frontier is empty.
|
|
||||||
|
|
||||||
## Shared understanding
|
|
||||||
|
|
||||||
Remove these instructions from `configs/nixos/AGENTS.md`:
|
|
||||||
|
|
||||||
- Hyprland session environment
|
|
||||||
- Firefox search engines
|
|
||||||
- Herdr shifted punctuation
|
|
||||||
- Pi extension source location
|
|
||||||
- Pi managed settings
|
|
||||||
- Nested guest mounts
|
|
||||||
- `deploy-to` behavior
|
|
||||||
- Pikachu deploy closures
|
|
||||||
- Public DNS boundary
|
|
||||||
- Hardware configuration
|
|
||||||
- Chaotic cache selection
|
|
||||||
- Module-relative paths
|
|
||||||
- Pikachu boot time
|
|
||||||
- Guest ACME evaluation
|
|
||||||
- systemd executable selection
|
|
||||||
|
|
||||||
Move these instructions intact to `src/skills/style/nix/rules/dotfiles.md`:
|
|
||||||
|
|
||||||
- Graphical application icons
|
|
||||||
- Guest ownership
|
|
||||||
- SOPS guest ownership
|
|
||||||
|
|
||||||
Move the generalized library-function qualification convention to
|
|
||||||
`src/skills/style/nix/rules/general.md`.
|
|
||||||
|
|
||||||
Move these instructions to code comments:
|
|
||||||
|
|
||||||
- Firefox extension storage: above `extensions.settings` in
|
|
||||||
`configs/nixos/modules/desktop/apps/firefox.nix`
|
|
||||||
- Nixvim package set: above `nixpkgs.useGlobalPackages` in
|
|
||||||
`configs/nixos/modules/shell/nvim/default.nix`
|
|
||||||
|
|
||||||
The review does not decide how to restore the auto-generated status of
|
|
||||||
`configs/nixos/hosts/pikachu/hardware-configuration.nix`.
|
|
||||||
|
|
||||||
The operator confirmed this design and authorized the edits.
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
# RomM migration design tree
|
|
||||||
|
|
||||||
## Known facts
|
|
||||||
|
|
||||||
- RomM is listed under the Pikachu and Pichu Proxmox migration.
|
|
||||||
- Pikachu already runs VLAN 20 guests with persistent storage.
|
|
||||||
- Nixpkgs provides a tested `services.romm` module at version 5.2.0.
|
|
||||||
- `services.romm.dataDir` defaults to `/var/lib/romm`.
|
|
||||||
- RomM stores the game library below `/var/lib/romm/library`.
|
|
||||||
- RomM stores saves, states, user-uploaded screenshots, and profile avatars below `/var/lib/romm/assets/users/<user-id>/`.
|
|
||||||
- RomM derives `<user-id>` from its database ID, so it is not a stable configured username.
|
|
||||||
- RomM stores scraped covers and provider-sourced game screenshots below `/var/lib/romm/resources/roms/<platform-id>/<rom-id>/`.
|
|
||||||
- RomM 5.4.0 exists upstream but Nixpkgs does not package it.
|
|
||||||
- RomM 5.3.0 requires an explicit library-structure declaration in `config.yml`.
|
|
||||||
- The 5.4.0 package phase must add the Structure A declaration for `roms/{platform}/{game}` and `bios/{platform}`.
|
|
||||||
- RomM 5.4.0 can modify ROM files in place only when library conversion is explicitly enabled.
|
|
||||||
- RomM 5.3.0 identifies hashed ROMs by CRC, MD5, and SHA-1 after a scan.
|
|
||||||
- RomM 5.3.0 matches moved or renamed hashed ROMs to their existing database records.
|
|
||||||
- This migration is a fresh installation.
|
|
||||||
- The new library will receive selected ROMs from the old installation.
|
|
||||||
- Copyparty serves `/pikachu/media` as its writable `/media` volume.
|
|
||||||
|
|
||||||
## Decision tree
|
|
||||||
|
|
||||||
- RomM deployment
|
|
||||||
- Target location
|
|
||||||
- Settled: Deploy a dedicated guest on Pikachu.
|
|
||||||
- Package source
|
|
||||||
- Settled: Start with Nixpkgs's RomM 5.2.0 package.
|
|
||||||
- Settled: Package RomM 5.4.0 locally as follow-up work.
|
|
||||||
- Persistent storage
|
|
||||||
- Settled: Use `/var/lib/romm` in the guest.
|
|
||||||
- Settled: Mount `/pikachu/data/romm` at `/var/lib/romm`.
|
|
||||||
- Settled: Mount the ROM library separately at `/var/lib/romm/library`.
|
|
||||||
- Settled: Mount user assets separately for Copyparty browsing.
|
|
||||||
- Settled: Keep scraped resources in `/pikachu/data/romm`.
|
|
||||||
- Settled: Mount `/pikachu/media/games/emulation/roms` at `/var/lib/romm/library/roms`.
|
|
||||||
- Settled: Mount `/pikachu/media/games/emulation/bios` at `/var/lib/romm/library/bios`.
|
|
||||||
- Settled: Mount `/pikachu/media/games/emulation/users` at `/var/lib/romm/assets/users`.
|
|
||||||
- Settled: Mount `/pikachu/data/romm/postgresql` at `/var/lib/postgresql`.
|
|
||||||
- Settled: Mount `/pikachu/data/romm/redis` at `/var/lib/redis-romm`.
|
|
||||||
- Guest networking
|
|
||||||
- Settled: Follow the standard guest-provisioning protocol, including its VLAN 20 DHCP reservation.
|
|
||||||
- Public route
|
|
||||||
- Settled: Retain the old installation's public domain.
|
|
||||||
- Metadata and authentication secrets
|
|
||||||
- Settled: Configure IGDB, ScreenScraper, and SteamGridDB.
|
|
||||||
- Settled: Use RomM's default metadata and artwork priority.
|
|
||||||
- Settled: Manage the 5.4.0 `config.yml` declaratively with Nix.
|
|
||||||
- Settled: Use RomM's default region and English-language priority.
|
|
||||||
- Save synchronization
|
|
||||||
- Settled: Disable automatic browser save synchronization in 5.4.0.
|
|
||||||
- Authentication
|
|
||||||
- Settled: Use RomM's built-in accounts initially.
|
|
||||||
- Deferred: Configure RomM as an OpenID Connect client after an identity provider exists.
|
|
||||||
- ROM selection and import
|
|
||||||
- Settled: The operator manually selects and copies ROMs after deployment.
|
|
||||||
- Settled: Do not create automatic import tooling or a ROM manifest.
|
|
||||||
- Capacity and service limits
|
|
||||||
- Settled: Limit the guest to `4G` memory, `200%` CPU, and `512` tasks.
|
|
||||||
- Library scanning
|
|
||||||
- Settled: Enable the filesystem watcher for library changes.
|
|
||||||
- Cutover and retirement
|
|
||||||
- Out of scope: The operator manages the old installation and public-domain cutover outside this monorepo.
|
|
||||||
- Work phases
|
|
||||||
- Settled: Create a RomM guest module with Nixpkgs's package in [[0014-romm-guest]].
|
|
||||||
- Settled: Package RomM 5.4.0 locally and reuse `services.romm` in [[0015-romm-5-4-package]].
|
|
||||||
- Settled: Move selected ROMs after the service deployment is ready in [[0016-romm-manual-rom-migration]].
|
|
||||||
|
|
||||||
## Settled decisions
|
|
||||||
|
|
||||||
- **Migration model:** Start with a fresh RomM installation.
|
|
||||||
- **Data scope:** Copy selected ROMs from the old installation.
|
|
||||||
- **Old application state:** Do not migrate the database, users, sessions, or job queue.
|
|
||||||
- **Deployment target:** Deploy a dedicated guest on Pikachu.
|
|
||||||
- **Initial package:** Use Nixpkgs's RomM 5.2.0 package.
|
|
||||||
- **Package follow-up:** Package RomM 5.4.0 locally.
|
|
||||||
- **Work phases:** Deploy the guest, update the package, then import selected ROMs.
|
|
||||||
- **Guest data directory:** Use RomM's default `/var/lib/romm`.
|
|
||||||
- **Host application data:** Mount `/pikachu/data/romm` at `/var/lib/romm`.
|
|
||||||
- **ROM library:** Mount it separately at `/var/lib/romm/library`.
|
|
||||||
- **Storage classification:** Preserve the distinction between application data and browsable media.
|
|
||||||
- **User assets:** Mount `/pikachu/media/games/emulation/users` at `/var/lib/romm/assets/users` to expose saves, states, user-uploaded screenshots, and profile avatars through Copyparty.
|
|
||||||
- **ROM paths:** Mount `/pikachu/media/games/emulation/roms` and `/pikachu/media/games/emulation/bios` at the corresponding children of `/var/lib/romm/library`.
|
|
||||||
- **Guest networking:** Follow the standard guest-provisioning protocol.
|
|
||||||
- **Public route:** Retain the old installation's public domain.
|
|
||||||
- **Metadata providers:** Configure IGDB, ScreenScraper, and SteamGridDB in both package phases.
|
|
||||||
- **Metadata priority:** Use RomM's default metadata and artwork priority.
|
|
||||||
- **RomM configuration:** Manage the required 5.4.0 `config.yml` declaratively with Nix.
|
|
||||||
- **Scan region and language priority:** Use RomM's defaults.
|
|
||||||
- **Initial authentication:** Use RomM's built-in accounts.
|
|
||||||
- **OIDC follow-up:** Track it in [[0013-oidc-provider]].
|
|
||||||
- **ROM migration:** The operator manually selects and copies ROMs after deployment.
|
|
||||||
- **Guest limits:** Cap the guest at `4G` memory, `200%` CPU, and `512` tasks.
|
|
||||||
- **Library watcher:** Enable the filesystem watcher.
|
|
||||||
- **PostgreSQL:** Mount `/pikachu/data/romm/postgresql` at `/var/lib/postgresql`.
|
|
||||||
- **Redis:** Mount `/pikachu/data/romm/redis` at `/var/lib/redis-romm`.
|
|
||||||
- **Browser save synchronization:** Disable it in 5.4.0 until browser emulation is needed.
|
|
||||||
|
|
||||||
## Frontier
|
|
||||||
|
|
||||||
No in-scope decisions remain.
|
|
||||||
|
|
||||||
## Next question
|
|
||||||
|
|
||||||
None. Present the settled design for confirmation before implementation.
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# TL;DR command design tree
|
|
||||||
|
|
||||||
This tree reviews the implementation decisions in `tldr-design.md`.
|
|
||||||
|
|
||||||
## Settled decisions
|
|
||||||
|
|
||||||
- **Command form and scope:** Add the project prompt template `.pi/prompts/tldr.md`.
|
|
||||||
- **Explicit source interface:** Interpret command arguments as a free-form source request.
|
|
||||||
- **Default source:** Summarize the preceding agent response when no source request exists.
|
|
||||||
- **Summary shape:** Produce a compact summary without a fixed format.
|
|
||||||
- **Unavailable or unclear sources:** Add no special handling policy.
|
|
||||||
|
|
||||||
## Decision tree
|
|
||||||
|
|
||||||
- Command behavior
|
|
||||||
- Source selection
|
|
||||||
- Settled: Use the preceding agent response without arguments.
|
|
||||||
- Settled: Interpret arguments as a free-form source request.
|
|
||||||
- Output
|
|
||||||
- Settled: Produce a compact summary without a fixed format.
|
|
||||||
- Summary language
|
|
||||||
- Settled: Do not prescribe a language policy.
|
|
||||||
- Error behavior
|
|
||||||
- Settled: Add no special handling policy for unavailable or unclear sources.
|
|
||||||
|
|
||||||
## Frontier
|
|
||||||
|
|
||||||
The frontier is empty.
|
|
||||||
|
|
||||||
## Shared understanding pending confirmation
|
|
||||||
|
|
||||||
The project gets a `/tldr` prompt command in `.pi/prompts/tldr.md`.
|
|
||||||
Without arguments, it summarizes the preceding agent response.
|
|
||||||
With arguments, it interprets them as a free-form source request.
|
|
||||||
It produces a compact summary with no fixed format or language policy.
|
|
||||||
The prompt adds no special policy for unavailable or unclear sources.
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# TL;DR command design
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Add a Pi command that summarizes the preceding agent response by default.
|
|
||||||
An explicit command argument identifies the source to summarize.
|
|
||||||
|
|
||||||
## Decision tree
|
|
||||||
|
|
||||||
- Command form and scope
|
|
||||||
- Settled: Create a project prompt template at `.pi/prompts/tldr.md`.
|
|
||||||
- Command interface
|
|
||||||
- Settled: Pass arguments through as a free-form source request.
|
|
||||||
- Default source selection
|
|
||||||
- Settled: Summarize the preceding agent response when no source request exists.
|
|
||||||
- Summary format and length
|
|
||||||
- Settled: Use a compact, format-free summary.
|
|
||||||
- Ambiguous-source behavior
|
|
||||||
- Settled: Add no special handling policy.
|
|
||||||
|
|
||||||
## Settled decisions
|
|
||||||
|
|
||||||
- **Q1 — Command form and scope:** Create a project prompt template.
|
|
||||||
- **Q2 — Explicit source interface:** Accept a free-form source request.
|
|
||||||
- **Default source selection:** Summarize the preceding agent response when the command has no arguments.
|
|
||||||
- **Q3 — Summary shape:** Use a compact, format-free summary.
|
|
||||||
- **Q4 — Unavailable source:** Add no special handling policy.
|
|
||||||
|
|
||||||
## Frontier
|
|
||||||
|
|
||||||
The frontier is empty.
|
|
||||||
|
|
||||||
## Shared understanding pending confirmation
|
|
||||||
|
|
||||||
The project gets a `/tldr` prompt command in `.pi/prompts/tldr.md`.
|
|
||||||
Without arguments, it summarizes the preceding agent response.
|
|
||||||
With arguments, it interprets them as a free-form source request.
|
|
||||||
It produces a compact summary with no fixed format.
|
|
||||||
The prompt adds no special policy for unavailable or unclear sources.
|
|
||||||
@@ -1,939 +0,0 @@
|
|||||||
# Valheim craft and build checklist
|
|
||||||
|
|
||||||
This checklist groups craftable inventory items and non-structural build pieces by biome progression.
|
|
||||||
It excludes floors, walls, roofs, beams, poles, stairs, doors, gates, windows, and generic structure pieces.
|
|
||||||
Biome placement uses the earliest ordinary unlock from recipe materials or required stations.
|
|
||||||
|
|
||||||
## Meadows
|
|
||||||
|
|
||||||
### Buildables
|
|
||||||
|
|
||||||
- [x] Workbench
|
|
||||||
- [x] Chopping Block
|
|
||||||
- [x] Tanning Rack
|
|
||||||
- [x] Cooking Station
|
|
||||||
- [x] Beehive
|
|
||||||
- [x] Bed
|
|
||||||
- [x] Chest
|
|
||||||
- [x] Sign
|
|
||||||
- [x] Campfire
|
|
||||||
- [x] Standing Wood Torch
|
|
||||||
- [x] Deer Rug
|
|
||||||
- [x] Mysterious Rock
|
|
||||||
- [x] Wood Stack
|
|
||||||
- [x] Stone Pile
|
|
||||||
- [x] Flint Pile
|
|
||||||
|
|
||||||
### Equipment and inventory
|
|
||||||
|
|
||||||
#### Weapons and tools
|
|
||||||
|
|
||||||
- [x] Stone Axe
|
|
||||||
- [x] Flint Axe
|
|
||||||
- [x] Early Axes
|
|
||||||
- [x] Club
|
|
||||||
- [x] Torch
|
|
||||||
- [x] Flint Spear
|
|
||||||
- [x] Flint Knife
|
|
||||||
- [x] Crude Bow
|
|
||||||
- [x] Hoe
|
|
||||||
- [x] Antler Pickaxe
|
|
||||||
|
|
||||||
#### Armor
|
|
||||||
|
|
||||||
- [x] Rag Tunic
|
|
||||||
- [x] Rag Pants
|
|
||||||
- [x] Leather Helmet
|
|
||||||
- [x] Leather Tunic
|
|
||||||
- [x] Leather Pants
|
|
||||||
- [x] Deer Hide Cape
|
|
||||||
|
|
||||||
#### Shields
|
|
||||||
|
|
||||||
- [x] Wood Shield
|
|
||||||
- [x] Wood Tower Shield
|
|
||||||
- [x] Bone Tower Shield
|
|
||||||
|
|
||||||
#### Ammunition
|
|
||||||
|
|
||||||
- [x] Wood Arrow
|
|
||||||
- [x] Fire Arrow
|
|
||||||
- [x] Flinthead Arrow
|
|
||||||
|
|
||||||
#### Utility
|
|
||||||
|
|
||||||
- [x] Tankard
|
|
||||||
|
|
||||||
#### Fireworks
|
|
||||||
|
|
||||||
- [ ] Red fireworks
|
|
||||||
- [ ] Yellow fireworks
|
|
||||||
|
|
||||||
### Food, mead, and bait
|
|
||||||
|
|
||||||
- [x] Grilled neck tail
|
|
||||||
- [ ] Cooked boar meat
|
|
||||||
- [x] Honey
|
|
||||||
- [x] Cooked deer meat
|
|
||||||
- [x] Whole roasted Meadow boar
|
|
||||||
|
|
||||||
## Black Forest
|
|
||||||
|
|
||||||
### Buildables
|
|
||||||
|
|
||||||
- [x] Adze
|
|
||||||
- [x] Cauldron
|
|
||||||
- [x] Mead Ketill
|
|
||||||
- [x] Fermenter
|
|
||||||
- [x] Forge
|
|
||||||
- [x] Forge Cooler
|
|
||||||
- [x] Anvils
|
|
||||||
- [x] Smelter
|
|
||||||
- [x] Charcoal Kiln
|
|
||||||
- [ ] Barrel
|
|
||||||
- [ ] Stool
|
|
||||||
- [ ] Wood Chair
|
|
||||||
- [ ] Sitting Log
|
|
||||||
- [ ] Wood Bench
|
|
||||||
- [x] Table
|
|
||||||
- [x] Item Stand — vertical
|
|
||||||
- [x] Item Stand — horizontal
|
|
||||||
- [ ] Cartography Table
|
|
||||||
- [ ] Barber Station
|
|
||||||
- [ ] Archery Target
|
|
||||||
- [ ] T.W.I.G.
|
|
||||||
- [ ] Wood Dragon Adornment
|
|
||||||
- [ ] Sconce
|
|
||||||
- [ ] Fey Lights
|
|
||||||
- [ ] Flower Garland
|
|
||||||
- [x] Bearskin Rug
|
|
||||||
- [ ] Black Banner
|
|
||||||
- [ ] Blue Banner
|
|
||||||
- [ ] White and Red Striped Banner
|
|
||||||
- [ ] Yellow Banner
|
|
||||||
- [ ] Purple Banner
|
|
||||||
- [ ] Finewood Stack
|
|
||||||
- [ ] Corewood Stack
|
|
||||||
- [x] Coal Pile
|
|
||||||
- [x] Bone Stack
|
|
||||||
- [x] Coin Stack
|
|
||||||
- [x] Coin Pile
|
|
||||||
- [x] Tin Stack
|
|
||||||
- [x] Copper Stack
|
|
||||||
- [x] Bronze Stack
|
|
||||||
- [x] Cart
|
|
||||||
- [x] Portal
|
|
||||||
- [ ] Sharp Stakes
|
|
||||||
- [ ] Ward
|
|
||||||
|
|
||||||
### Equipment and inventory
|
|
||||||
|
|
||||||
#### Weapons and tools
|
|
||||||
|
|
||||||
- [x] Bronze Axe
|
|
||||||
- [x] Bronze Pickaxe
|
|
||||||
- [x] Cultivator
|
|
||||||
- [x] Butcher Knife
|
|
||||||
- [x] Wooden Sword
|
|
||||||
- [x] Stagbreaker
|
|
||||||
- [x] Bronze Mace
|
|
||||||
- [x] Bronze Sword
|
|
||||||
- [x] Bronze Spear
|
|
||||||
- [x] Bronze Atgeir
|
|
||||||
- [x] Copper Knife
|
|
||||||
- [x] Paws of the Bear
|
|
||||||
- [x] Finewood Bow
|
|
||||||
|
|
||||||
#### Armor
|
|
||||||
|
|
||||||
- [x] Troll Leather Helmet
|
|
||||||
- [x] Troll Leather Tunic
|
|
||||||
- [x] Troll Leather Pants
|
|
||||||
- [x] Troll Hide Cape
|
|
||||||
- [x] Headdress of the Bear
|
|
||||||
- [x] Patterns of the Bear
|
|
||||||
- [x] Loincloth of the Bear
|
|
||||||
- [x] Bronze Helmet
|
|
||||||
- [x] Bronze Plate Cuirass
|
|
||||||
- [x] Bronze Plate Leggings
|
|
||||||
- [x] Celebratory Cap
|
|
||||||
|
|
||||||
#### Shield
|
|
||||||
|
|
||||||
- [x] Bronze Buckler
|
|
||||||
|
|
||||||
#### Ammunition
|
|
||||||
|
|
||||||
- [x] Bronzehead Arrow
|
|
||||||
|
|
||||||
#### Trinkets
|
|
||||||
|
|
||||||
- [x] Heart of the Forest
|
|
||||||
- [x] Bronze Pendant
|
|
||||||
|
|
||||||
#### Utility
|
|
||||||
|
|
||||||
- [x] Mead Horn of Odin
|
|
||||||
|
|
||||||
#### Fireworks
|
|
||||||
|
|
||||||
- [ ] Blue fireworks
|
|
||||||
- [ ] Cyan fireworks
|
|
||||||
|
|
||||||
### Food, mead, and bait
|
|
||||||
|
|
||||||
- [ ] Carrot
|
|
||||||
- [ ] Boar jerky
|
|
||||||
- [ ] Cooked bear meat
|
|
||||||
- [ ] Minced meat sauce
|
|
||||||
- [ ] Queen's jam
|
|
||||||
- [ ] Carrot soup
|
|
||||||
- [ ] Deer stew
|
|
||||||
- [ ] Pulled Bear
|
|
||||||
- [ ] Black Forest buffet platter
|
|
||||||
- [ ] Mead base: Minor healing `[INFERENCE]`
|
|
||||||
- [ ] Minor healing mead `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Minor stamina `[INFERENCE]`
|
|
||||||
- [ ] Minor stamina mead `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Poison resistance `[INFERENCE]`
|
|
||||||
- [ ] Poison resistance mead `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Tasty `[INFERENCE]`
|
|
||||||
- [ ] Tasty mead `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Vananidir `[INFERENCE]`
|
|
||||||
- [ ] Draught of Vananidir `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Ratatosk `[INFERENCE]`
|
|
||||||
- [ ] Tonic of Ratatosk `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Troll endurance `[INFERENCE]`
|
|
||||||
- [ ] Mead of Troll endurance `[INFERENCE]`
|
|
||||||
- [ ] Mossy fishing bait
|
|
||||||
|
|
||||||
## Ocean
|
|
||||||
|
|
||||||
### Buildables
|
|
||||||
|
|
||||||
- [ ] Raft
|
|
||||||
- [ ] Karve
|
|
||||||
- [ ] Longship
|
|
||||||
|
|
||||||
### Equipment and inventory
|
|
||||||
|
|
||||||
#### Weapons and tools
|
|
||||||
|
|
||||||
- [ ] Abyssal Razor
|
|
||||||
- [ ] Abyssal Harpoon
|
|
||||||
|
|
||||||
#### Trinket
|
|
||||||
|
|
||||||
- [ ] Fins of Destiny
|
|
||||||
|
|
||||||
### Food, mead, and bait
|
|
||||||
|
|
||||||
- [ ] Cooked serpent meat `[INFERENCE]`
|
|
||||||
- [ ] Serpent stew `[INFERENCE]`
|
|
||||||
- [ ] Sailor's bounty
|
|
||||||
- [ ] Heavy fishing bait
|
|
||||||
|
|
||||||
## Swamp
|
|
||||||
|
|
||||||
### Buildables
|
|
||||||
|
|
||||||
- [ ] Birds' Nest
|
|
||||||
- [ ] Spice Rack
|
|
||||||
- [ ] Stonecutter
|
|
||||||
- [ ] Food Preparation Table
|
|
||||||
- [ ] Iron Cooking Station
|
|
||||||
- [ ] Forge Bellows
|
|
||||||
- [ ] Grinding Wheel
|
|
||||||
- [ ] Smith's Anvil
|
|
||||||
- [ ] Forge Tool Rack
|
|
||||||
- [ ] Obliterator
|
|
||||||
- [ ] Bonfire
|
|
||||||
- [ ] Hearth
|
|
||||||
- [ ] Hanging Brazier
|
|
||||||
- [ ] Standing Iron Torch
|
|
||||||
- [ ] Standing Blue-burning Iron Torch
|
|
||||||
- [ ] Standing Green-burning Iron Torch
|
|
||||||
- [ ] Resin Candle
|
|
||||||
- [ ] Reinforced Chest
|
|
||||||
- [ ] Personal Chest
|
|
||||||
- [ ] Armour Stand
|
|
||||||
- [ ] Raven Throne
|
|
||||||
- [ ] Red Banner
|
|
||||||
- [ ] Green Banner
|
|
||||||
- [ ] Iron Stack
|
|
||||||
- [ ] Iron Fire Pit
|
|
||||||
|
|
||||||
### Equipment and inventory
|
|
||||||
|
|
||||||
#### Weapons and tools
|
|
||||||
|
|
||||||
- [ ] Iron Axe
|
|
||||||
- [ ] Iron Pickaxe
|
|
||||||
- [ ] Battleaxe
|
|
||||||
- [ ] Iron Mace
|
|
||||||
- [ ] Iron Sledge
|
|
||||||
- [ ] Iron Sword
|
|
||||||
- [ ] Ancient Bark Spear
|
|
||||||
- [ ] Iron Atgeir
|
|
||||||
- [ ] Huntsman Bow
|
|
||||||
|
|
||||||
#### Armor
|
|
||||||
|
|
||||||
- [ ] Root Mask
|
|
||||||
- [ ] Root Harnesk
|
|
||||||
- [ ] Root Leggings
|
|
||||||
- [ ] Iron Helmet
|
|
||||||
- [ ] Iron Scale Mail
|
|
||||||
- [ ] Iron Greaves
|
|
||||||
|
|
||||||
#### Shields
|
|
||||||
|
|
||||||
- [ ] Banded Shield
|
|
||||||
- [ ] Shield of Roots
|
|
||||||
- [ ] Iron Buckler
|
|
||||||
- [ ] Iron Tower Shield
|
|
||||||
- [ ] Serpent Scale Shield
|
|
||||||
|
|
||||||
#### Ammunition
|
|
||||||
|
|
||||||
- [ ] Ironhead Arrow
|
|
||||||
|
|
||||||
#### Bombs
|
|
||||||
|
|
||||||
- [ ] Ooze Bomb
|
|
||||||
- [ ] Blob Bomb: Poison
|
|
||||||
- [ ] Blob Bomb: Elite poison
|
|
||||||
|
|
||||||
#### Fishing bait
|
|
||||||
|
|
||||||
- [ ] Mossy Fishing Bait
|
|
||||||
- [ ] Sticky Fishing Bait
|
|
||||||
- [ ] Heavy Fishing Bait
|
|
||||||
|
|
||||||
#### Trinkets
|
|
||||||
|
|
||||||
- [ ] Iron Brooch
|
|
||||||
- [ ] Nimble Anklet
|
|
||||||
|
|
||||||
#### Utility
|
|
||||||
|
|
||||||
- [ ] Horn of Celebration
|
|
||||||
|
|
||||||
#### Fireworks
|
|
||||||
|
|
||||||
- [ ] Green fireworks
|
|
||||||
- [ ] Purple fireworks
|
|
||||||
|
|
||||||
### Food, mead, and bait
|
|
||||||
|
|
||||||
- [ ] Cooked fish
|
|
||||||
- [ ] Muckshake
|
|
||||||
- [ ] Turnip stew
|
|
||||||
- [ ] Sausages
|
|
||||||
- [ ] Black soup
|
|
||||||
- [ ] Swamp dweller's delight
|
|
||||||
- [ ] Mead base: Frost resistance `[INFERENCE]`
|
|
||||||
- [ ] Frost resistance mead `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Medium healing `[INFERENCE]`
|
|
||||||
- [ ] Medium healing mead `[INFERENCE]`
|
|
||||||
- [ ] Sticky fishing bait
|
|
||||||
|
|
||||||
## Mountain
|
|
||||||
|
|
||||||
### Buildables
|
|
||||||
|
|
||||||
- [ ] Tool Shelf
|
|
||||||
- [ ] Butcher's Table
|
|
||||||
- [ ] Dragon Bed
|
|
||||||
- [ ] Stone Throne
|
|
||||||
- [ ] Standing Brazier
|
|
||||||
- [ ] Blue Standing Brazier
|
|
||||||
- [ ] Wolf Rug
|
|
||||||
- [ ] Treasure Chest
|
|
||||||
- [ ] Silver Stack
|
|
||||||
|
|
||||||
### Equipment and inventory
|
|
||||||
|
|
||||||
#### Weapons and tools
|
|
||||||
|
|
||||||
- [ ] Crystal Battleaxe
|
|
||||||
- [ ] Frostner
|
|
||||||
- [ ] Silver Sword
|
|
||||||
- [ ] Fang Spear
|
|
||||||
- [ ] Silver Knife
|
|
||||||
- [ ] Flesh Rippers
|
|
||||||
- [ ] Draugr Fang
|
|
||||||
- [ ] Scythe
|
|
||||||
|
|
||||||
#### Armor
|
|
||||||
|
|
||||||
- [ ] Fenris Hood
|
|
||||||
- [ ] Fenris Coat
|
|
||||||
- [ ] Fenris Leggings
|
|
||||||
- [ ] Drake Helmet
|
|
||||||
- [ ] Wolf Hide Chestpiece
|
|
||||||
- [ ] Wolf Hide Trousers
|
|
||||||
- [ ] Wolf Fur Cape
|
|
||||||
|
|
||||||
#### Shield
|
|
||||||
|
|
||||||
- [ ] Silver Shield
|
|
||||||
|
|
||||||
#### Ammunition
|
|
||||||
|
|
||||||
- [ ] Silver Arrow
|
|
||||||
- [ ] Obsidian Arrow
|
|
||||||
- [ ] Poison Arrow
|
|
||||||
- [ ] Frost Arrow
|
|
||||||
|
|
||||||
#### Bomb
|
|
||||||
|
|
||||||
- [ ] Blob Bomb: Frost
|
|
||||||
|
|
||||||
#### Fishing bait
|
|
||||||
|
|
||||||
- [ ] Cold Fishing Bait
|
|
||||||
- [ ] Frosty Fishing Bait
|
|
||||||
|
|
||||||
#### Trinkets
|
|
||||||
|
|
||||||
- [ ] Wolf Sight
|
|
||||||
- [ ] Crystal Heart
|
|
||||||
|
|
||||||
### Food, mead, and bait
|
|
||||||
|
|
||||||
- [ ] Onion
|
|
||||||
- [ ] Cooked wolf meat
|
|
||||||
- [ ] Wolf jerky
|
|
||||||
- [ ] Onion soup
|
|
||||||
- [ ] Wolf skewer
|
|
||||||
- [ ] Eyescream
|
|
||||||
- [ ] Hearty Mountain logger's stew
|
|
||||||
- [ ] Mead base: Animal whispers `[INFERENCE]`
|
|
||||||
- [ ] Brew of animal whispers `[INFERENCE]`
|
|
||||||
- [ ] Cold fishing bait
|
|
||||||
- [ ] Frosty fishing bait `[INFERENCE]`
|
|
||||||
|
|
||||||
## Plains
|
|
||||||
|
|
||||||
### Buildables
|
|
||||||
|
|
||||||
- [ ] Artisan Table
|
|
||||||
- [ ] Blast Furnace
|
|
||||||
- [ ] Spinning Wheel
|
|
||||||
- [ ] Windmill
|
|
||||||
- [ ] Stone Oven
|
|
||||||
- [ ] Pots and Pans
|
|
||||||
- [ ] Darkwood Chair
|
|
||||||
- [ ] Long Heavy Table
|
|
||||||
- [ ] Round Table
|
|
||||||
- [ ] Hot Tub
|
|
||||||
- [ ] Black Metal Chest
|
|
||||||
- [ ] Lox Rug
|
|
||||||
- [ ] Straw
|
|
||||||
- [ ] Red Jute Carpet
|
|
||||||
- [ ] Red Jute Curtain
|
|
||||||
- [ ] Raven Adornment
|
|
||||||
- [ ] Wolf Adornment
|
|
||||||
- [ ] Blue, Red and White Banner
|
|
||||||
- [ ] White and Blue Striped Banner
|
|
||||||
- [ ] Orange Banner
|
|
||||||
- [ ] White Banner
|
|
||||||
- [ ] Black Metal Stack
|
|
||||||
|
|
||||||
### Equipment and inventory
|
|
||||||
|
|
||||||
#### Weapons and tools
|
|
||||||
|
|
||||||
- [ ] Black Metal Axe
|
|
||||||
- [ ] Black Metal Battleaxe
|
|
||||||
- [ ] Porcupine
|
|
||||||
- [ ] Black Metal Sword
|
|
||||||
- [ ] Black Metal Atgeir
|
|
||||||
- [ ] Black Metal Knife
|
|
||||||
- [ ] Vilebone Maulclaws
|
|
||||||
|
|
||||||
#### Armor
|
|
||||||
|
|
||||||
- [ ] Padded Helmet
|
|
||||||
- [ ] Padded Cuirass
|
|
||||||
- [ ] Padded Greaves
|
|
||||||
- [ ] Linen Cape
|
|
||||||
- [ ] Lox Cape
|
|
||||||
- [ ] Lox Fur Hood
|
|
||||||
- [ ] Lox Fur Jacket
|
|
||||||
- [ ] Lox Fur Trousers
|
|
||||||
- [ ] Vilebone Visage
|
|
||||||
- [ ] Vilebone Cage
|
|
||||||
- [ ] Vilebone Drapes
|
|
||||||
|
|
||||||
#### Shields
|
|
||||||
|
|
||||||
- [ ] Black Metal Shield
|
|
||||||
- [ ] Black Metal Tower Shield
|
|
||||||
|
|
||||||
#### Ammunition
|
|
||||||
|
|
||||||
- [ ] Needle Arrow
|
|
||||||
- [ ] Wooden Missile
|
|
||||||
- [ ] Black Metal Missile
|
|
||||||
|
|
||||||
#### Bomb
|
|
||||||
|
|
||||||
- [ ] Blob Bomb: Tar
|
|
||||||
|
|
||||||
#### Fishing bait
|
|
||||||
|
|
||||||
- [ ] Stingy Fishing Bait
|
|
||||||
- [ ] Misty Fishing Bait
|
|
||||||
|
|
||||||
#### Trinkets
|
|
||||||
|
|
||||||
- [ ] Bracelets of the Brave
|
|
||||||
- [ ] Evasion Mantle
|
|
||||||
|
|
||||||
#### Accessories and utility
|
|
||||||
|
|
||||||
- [ ] Wisplight
|
|
||||||
- [ ] Lox Saddle
|
|
||||||
|
|
||||||
### Food, mead, and bait
|
|
||||||
|
|
||||||
- [ ] Cooked egg
|
|
||||||
- [ ] Cooked chicken meat
|
|
||||||
- [ ] Cooked lox meat
|
|
||||||
- [ ] Fish wraps
|
|
||||||
- [ ] Lox meat pie
|
|
||||||
- [ ] Blood pudding
|
|
||||||
- [ ] Bread
|
|
||||||
- [ ] Frosted sweetbread
|
|
||||||
- [ ] Plains pie picnic
|
|
||||||
- [ ] Barley wine base: Fire resistance `[INFERENCE]`
|
|
||||||
- [ ] Fire resistance barley wine `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Medium stamina `[INFERENCE]`
|
|
||||||
- [ ] Medium stamina mead `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Berserkir `[INFERENCE]`
|
|
||||||
- [ ] Berserkir mead `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Anti-sting `[INFERENCE]`
|
|
||||||
- [ ] Anti-sting concoction `[INFERENCE]`
|
|
||||||
- [ ] Stingy fishing bait
|
|
||||||
- [ ] Misty fishing bait `[INFERENCE]`
|
|
||||||
|
|
||||||
## Mistlands
|
|
||||||
|
|
||||||
### Buildables
|
|
||||||
|
|
||||||
- [ ] Wisp Fountain
|
|
||||||
- [ ] Artisan Press
|
|
||||||
- [ ] Mortar and Pestle
|
|
||||||
- [ ] Black Forge
|
|
||||||
- [ ] Black Forge Cooler
|
|
||||||
- [ ] Vice
|
|
||||||
- [ ] Galdr Table
|
|
||||||
- [ ] Rune Table
|
|
||||||
- [ ] Unfading Candles
|
|
||||||
- [ ] Sap Extractor
|
|
||||||
- [ ] Eitr Refinery
|
|
||||||
- [ ] Dvergr Wall Lantern
|
|
||||||
- [ ] Dvergr Pole Lantern
|
|
||||||
- [ ] Wisp Torch
|
|
||||||
- [ ] Black Marble Bench
|
|
||||||
- [ ] Black Marble Table
|
|
||||||
- [ ] Black Marble Throne
|
|
||||||
- [ ] Hare Rug
|
|
||||||
- [ ] Blue Jute Carpet
|
|
||||||
- [ ] Blue Jute Curtain
|
|
||||||
- [ ] Blue Jute Drapes
|
|
||||||
- [ ] Yggdrasil Wood Stack
|
|
||||||
- [ ] Black Marble Pile
|
|
||||||
- [ ] Dvergr Sharp Stakes
|
|
||||||
- [ ] Trap
|
|
||||||
- [ ] Ballista
|
|
||||||
|
|
||||||
### Equipment and inventory
|
|
||||||
|
|
||||||
#### Weapons and tools
|
|
||||||
|
|
||||||
- [ ] Jotun Bane
|
|
||||||
- [ ] Skull Splittur
|
|
||||||
- [ ] Demolisher
|
|
||||||
- [ ] Mistwalker
|
|
||||||
- [ ] Krom
|
|
||||||
- [ ] Carapace Spear
|
|
||||||
- [ ] Himminafl
|
|
||||||
- [ ] Skoll and Hati
|
|
||||||
- [ ] Spinesnap
|
|
||||||
- [ ] Arbalest
|
|
||||||
- [ ] Staff of Frost
|
|
||||||
- [ ] Staff of Embers
|
|
||||||
- [ ] Staff of Protection
|
|
||||||
- [ ] Dead Raiser
|
|
||||||
- [ ] Black Metal Pickaxe
|
|
||||||
- [ ] Dvergr Lantern
|
|
||||||
|
|
||||||
#### Armor
|
|
||||||
|
|
||||||
- [ ] Carapace Helmet
|
|
||||||
- [ ] Carapace Breastplate
|
|
||||||
- [ ] Carapace Greaves
|
|
||||||
- [ ] Eitr-weave Hood
|
|
||||||
- [ ] Eitr-weave Robe
|
|
||||||
- [ ] Eitr-weave Trousers
|
|
||||||
- [ ] Feather Cape
|
|
||||||
|
|
||||||
#### Shields
|
|
||||||
|
|
||||||
- [ ] Carapace Shield
|
|
||||||
- [ ] Carapace Buckler
|
|
||||||
|
|
||||||
#### Ammunition
|
|
||||||
|
|
||||||
- [ ] Carapace Arrow
|
|
||||||
- [ ] Bone Bolt
|
|
||||||
- [ ] Iron Bolt
|
|
||||||
- [ ] Black Metal Bolt
|
|
||||||
- [ ] Carapace Bolt
|
|
||||||
|
|
||||||
#### Bomb
|
|
||||||
|
|
||||||
- [ ] Bile Bomb
|
|
||||||
|
|
||||||
#### Trinkets
|
|
||||||
|
|
||||||
- [ ] Pulsating Earrings
|
|
||||||
- [ ] Resounding Shackle
|
|
||||||
|
|
||||||
#### Utility
|
|
||||||
|
|
||||||
- [ ] Sealbreaker
|
|
||||||
|
|
||||||
### Food, mead, and bait
|
|
||||||
|
|
||||||
- [ ] Magecap
|
|
||||||
- [ ] Jotun puffs
|
|
||||||
- [ ] Cooked seeker meat
|
|
||||||
- [ ] Cooked hare meat
|
|
||||||
- [ ] Meat platter
|
|
||||||
- [ ] Honey glazed chicken
|
|
||||||
- [ ] Misthare supreme
|
|
||||||
- [ ] Salad
|
|
||||||
- [ ] Mushroom omelette
|
|
||||||
- [ ] Fish 'n' bread
|
|
||||||
- [ ] Stuffed mushroom
|
|
||||||
- [ ] Yggdrasil porridge
|
|
||||||
- [ ] Seeker aspic
|
|
||||||
- [ ] Mushrooms galore á la Mistlands
|
|
||||||
- [ ] Mead base: Major healing `[INFERENCE]`
|
|
||||||
- [ ] Major healing mead `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Lingering stamina `[INFERENCE]`
|
|
||||||
- [ ] Lingering stamina mead `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Minor eitr `[INFERENCE]`
|
|
||||||
- [ ] Minor eitr mead `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Lightfoot `[INFERENCE]`
|
|
||||||
- [ ] Lightfoot mead `[INFERENCE]`
|
|
||||||
|
|
||||||
## Ashlands
|
|
||||||
|
|
||||||
### Buildables
|
|
||||||
|
|
||||||
- [ ] Rolling Pins and Cutting Boards
|
|
||||||
- [ ] Metal Cutter
|
|
||||||
- [ ] Gem Cutter
|
|
||||||
- [ ] Feathery Wreath
|
|
||||||
- [ ] Eternal Pyre
|
|
||||||
- [ ] Shield Generator
|
|
||||||
- [ ] Ashwood Bed
|
|
||||||
- [ ] Ashwood Bench
|
|
||||||
- [ ] Bone Throne
|
|
||||||
- [ ] Grausten Chest
|
|
||||||
- [ ] Small Green Pot
|
|
||||||
- [ ] Medium Green Pot
|
|
||||||
- [ ] Large Green Pot
|
|
||||||
- [ ] Lava Lantern
|
|
||||||
- [ ] Asksvin Rug
|
|
||||||
- [ ] Asksvin Skeleton
|
|
||||||
- [ ] Ashwood Stack
|
|
||||||
- [ ] Grausten Pile
|
|
||||||
- [ ] Flametal Stack
|
|
||||||
- [ ] Pile of Skulls
|
|
||||||
- [ ] Portal Stone
|
|
||||||
- [ ] Drakkar
|
|
||||||
- [ ] Battering Ram
|
|
||||||
- [ ] Catapult
|
|
||||||
|
|
||||||
### Equipment and inventory
|
|
||||||
|
|
||||||
#### Weapons
|
|
||||||
|
|
||||||
- [ ] Berserkir Axes
|
|
||||||
- [ ] Bleeding Berserkir Axes
|
|
||||||
- [ ] Thundering Berserkir Axes
|
|
||||||
- [ ] Primal Berserkir Axes
|
|
||||||
- [ ] Flametal Mace
|
|
||||||
- [ ] Bloodgeon
|
|
||||||
- [ ] Storm Star
|
|
||||||
- [ ] Klossen
|
|
||||||
- [ ] Nidhögg
|
|
||||||
- [ ] Nidhögg the Bleeding
|
|
||||||
- [ ] Nidhögg the Thundering
|
|
||||||
- [ ] Nidhögg the Primal
|
|
||||||
- [ ] Dyrnwyn
|
|
||||||
- [ ] Slayer
|
|
||||||
- [ ] Brutal Slayer
|
|
||||||
- [ ] Scourging Slayer
|
|
||||||
- [ ] Primal Slayer
|
|
||||||
- [ ] Splitnir
|
|
||||||
- [ ] Splitnir the Bleeding
|
|
||||||
- [ ] Splitnir the Storming
|
|
||||||
- [ ] Splitnir the Primal
|
|
||||||
- [ ] Ash Fang
|
|
||||||
- [ ] Blood Fang
|
|
||||||
- [ ] Storm Fang
|
|
||||||
- [ ] Root Fang
|
|
||||||
- [ ] Ripper
|
|
||||||
- [ ] Wound Ripper
|
|
||||||
- [ ] Storm Ripper
|
|
||||||
- [ ] Root Ripper
|
|
||||||
- [ ] Staff of Fracturing
|
|
||||||
- [ ] Dundr
|
|
||||||
- [ ] Staff of the Wild
|
|
||||||
- [ ] Trollstav
|
|
||||||
|
|
||||||
#### Armor
|
|
||||||
|
|
||||||
- [ ] Hood of Embla
|
|
||||||
- [ ] Robes of Embla
|
|
||||||
- [ ] Trousers of Embla
|
|
||||||
- [ ] Hood of Ask
|
|
||||||
- [ ] Breastplate of Ask
|
|
||||||
- [ ] Trousers of Ask
|
|
||||||
- [ ] Flametal Helmet
|
|
||||||
- [ ] Flametal Breastplate
|
|
||||||
- [ ] Flametal Greaves
|
|
||||||
- [ ] Asksvin Cloak
|
|
||||||
- [ ] Ashen Cape
|
|
||||||
|
|
||||||
#### Shields
|
|
||||||
|
|
||||||
- [ ] Flametal Shield
|
|
||||||
- [ ] Flametal Tower Shield
|
|
||||||
|
|
||||||
#### Ammunition
|
|
||||||
|
|
||||||
- [ ] Charred Arrow
|
|
||||||
- [ ] Charred Bolt
|
|
||||||
- [ ] Flametal Missile
|
|
||||||
- [ ] Grausten Payload
|
|
||||||
- [ ] Explosive Payload
|
|
||||||
|
|
||||||
#### Bombs
|
|
||||||
|
|
||||||
- [ ] Smoke Bomb
|
|
||||||
- [ ] Basalt Bomb
|
|
||||||
- [ ] Blob Bomb: Lava
|
|
||||||
|
|
||||||
#### Fishing bait
|
|
||||||
|
|
||||||
- [ ] Hot Fishing Bait
|
|
||||||
|
|
||||||
#### Trinkets
|
|
||||||
|
|
||||||
- [ ] Jörmundling
|
|
||||||
- [ ] Brimstone
|
|
||||||
|
|
||||||
#### Utility
|
|
||||||
|
|
||||||
- [ ] Asksvin Saddle
|
|
||||||
- [ ] Bell
|
|
||||||
|
|
||||||
### Food, mead, and bait
|
|
||||||
|
|
||||||
- [ ] Vineberry cluster
|
|
||||||
- [ ] Cooked asksvin tail
|
|
||||||
- [ ] Cooked volture meat
|
|
||||||
- [ ] Cooked bonemaw meat
|
|
||||||
- [ ] Fiery svinstew
|
|
||||||
- [ ] Mashed meat
|
|
||||||
- [ ] Piquant pie
|
|
||||||
- [ ] Spicy marmalade
|
|
||||||
- [ ] Scorching medley
|
|
||||||
- [ ] Roasted crust pie
|
|
||||||
- [ ] Sizzling berry broth
|
|
||||||
- [ ] Sparkling shroomshake
|
|
||||||
- [ ] Marinated greens
|
|
||||||
- [ ] Ashlands gourmet bowl
|
|
||||||
- [ ] Mead base: Lingering health `[INFERENCE]`
|
|
||||||
- [ ] Lingering healing mead `[INFERENCE]`
|
|
||||||
- [ ] Mead base: Lingering eitr `[INFERENCE]`
|
|
||||||
- [ ] Lingering eitr mead `[INFERENCE]`
|
|
||||||
- [ ] Hot fishing bait
|
|
||||||
|
|
||||||
## Deep North
|
|
||||||
|
|
||||||
### Buildables
|
|
||||||
|
|
||||||
- [ ] Eternal Pyre
|
|
||||||
- [ ] Frigid Kiln
|
|
||||||
- [ ] Frost Foundry
|
|
||||||
- [ ] Smith's Aprons
|
|
||||||
- [ ] Smoker
|
|
||||||
- [ ] Standing Loom
|
|
||||||
- [ ] Timberwood Drawbridge
|
|
||||||
- [ ] Antler Throne
|
|
||||||
- [ ] Carved Bench
|
|
||||||
- [ ] Carved Chair
|
|
||||||
- [ ] Hooded Lantern
|
|
||||||
- [ ] Long Carved Table
|
|
||||||
- [ ] Moose Hide Carpet
|
|
||||||
- [ ] Sealskin Rug
|
|
||||||
- [ ] Snow Lantern
|
|
||||||
- [ ] Square Carved Table
|
|
||||||
- [ ] Wardrobe
|
|
||||||
- [ ] Bloodgold Stack
|
|
||||||
- [ ] Timberwood Stack
|
|
||||||
- [ ] Oat Straw `[INFERENCE]`
|
|
||||||
- [ ] Seed Kale
|
|
||||||
- [ ] Timberwood Sapling
|
|
||||||
|
|
||||||
### Equipment and inventory
|
|
||||||
|
|
||||||
#### Equipment and inventory
|
|
||||||
|
|
||||||
- [ ] Nord Atgeir
|
|
||||||
- [ ] Nord Axe
|
|
||||||
- [ ] Nord Bow
|
|
||||||
- [ ] Nord Crossbow
|
|
||||||
- [ ] Nord Dagger
|
|
||||||
- [ ] Nord Greataxe
|
|
||||||
- [ ] Nord Greatsword
|
|
||||||
- [ ] Nord Knucklechains
|
|
||||||
- [ ] Nord Mace
|
|
||||||
- [ ] Nord Sledge
|
|
||||||
- [ ] Nord Spear
|
|
||||||
- [ ] Nord Sword
|
|
||||||
- [ ] Frostfire Atgeir
|
|
||||||
- [ ] Frostfire Axe
|
|
||||||
- [ ] Frostfire Bow
|
|
||||||
- [ ] Frostfire Crossbow
|
|
||||||
- [ ] Frostfire Dagger
|
|
||||||
- [ ] Frostfire Greataxe
|
|
||||||
- [ ] Frostfire Greatsword
|
|
||||||
- [ ] Frostfire Knucklechains
|
|
||||||
- [ ] Frostfire Mace
|
|
||||||
- [ ] Frostfire Sledge
|
|
||||||
- [ ] Frostfire Spear
|
|
||||||
- [ ] Frostfire Sword
|
|
||||||
- [ ] Thunderblood Atgeir
|
|
||||||
- [ ] Thunderblood Axe
|
|
||||||
- [ ] Thunderblood Bow
|
|
||||||
- [ ] Thunderblood Crossbow
|
|
||||||
- [ ] Thunderblood Dagger
|
|
||||||
- [ ] Thunderblood Greataxe
|
|
||||||
- [ ] Thunderblood Greatsword
|
|
||||||
- [ ] Thunderblood Knucklechains
|
|
||||||
- [ ] Thunderblood Mace
|
|
||||||
- [ ] Thunderblood Sledge
|
|
||||||
- [ ] Thunderblood Spear
|
|
||||||
- [ ] Thunderblood Sword
|
|
||||||
- [ ] Echo Spike
|
|
||||||
- [ ] Lightning Strike
|
|
||||||
- [ ] Northern Vengeance
|
|
||||||
- [ ] Spirit Caller
|
|
||||||
- [ ] Nord Buckler
|
|
||||||
- [ ] Nord Greatshield
|
|
||||||
- [ ] Nord Shield
|
|
||||||
- [ ] Helmet of the Protector
|
|
||||||
- [ ] Breastplate of the Protector
|
|
||||||
- [ ] Trousers of the Protector
|
|
||||||
- [ ] Hood of the Vanguard
|
|
||||||
- [ ] Chestpiece of the Vanguard
|
|
||||||
- [ ] Trousers of the Vanguard
|
|
||||||
- [ ] Headdress of the Caller
|
|
||||||
- [ ] Robes of the Caller
|
|
||||||
- [ ] Trousers of the Caller
|
|
||||||
- [ ] Cape of the Caller
|
|
||||||
- [ ] Moose Hide Cape
|
|
||||||
- [ ] Crown of Valheim `[INFERENCE]`
|
|
||||||
- [ ] Fishing Hat
|
|
||||||
- [ ] Neckstabber `[INFERENCE]`
|
|
||||||
- [ ] Witch Crown
|
|
||||||
- [ ] Snow Shovel
|
|
||||||
- [ ] Intricate Key
|
|
||||||
- [ ] Moose Saddle `[INFERENCE]`
|
|
||||||
- [ ] Bloodgold Arrow
|
|
||||||
- [ ] Bloodgold Bolt
|
|
||||||
- [ ] Bloodgold Missile
|
|
||||||
- [ ] Bloodgold Payload
|
|
||||||
- [ ] Blob Bomb: Pulp `[INFERENCE]`
|
|
||||||
- [ ] Ember Charge
|
|
||||||
- [ ] Snowball
|
|
||||||
|
|
||||||
### Food, mead, and bait
|
|
||||||
|
|
||||||
- [ ] Baked Poteitr
|
|
||||||
- [ ] Cooked Moose Meat
|
|
||||||
- [ ] Cooked Seal Blubber `[INFERENCE]`
|
|
||||||
- [ ] Fish Soup `[INFERENCE]`
|
|
||||||
- [ ] Kale *(also the cultivator crop piece; listed once)*
|
|
||||||
- [ ] Kale Chips `[INFERENCE]`
|
|
||||||
- [ ] Lingonberries
|
|
||||||
- [ ] Lingonberry Juice
|
|
||||||
- [ ] Luminous Larva
|
|
||||||
- [ ] Meat In Bread
|
|
||||||
- [ ] Meatballs and Poteitr
|
|
||||||
- [ ] Oat Milk `[INFERENCE]`
|
|
||||||
- [ ] Oatmeal
|
|
||||||
- [ ] Oats `[INFERENCE]`
|
|
||||||
- [ ] Oven Pancake
|
|
||||||
- [ ] Pancakes
|
|
||||||
- [ ] Poteitr `[INFERENCE]`
|
|
||||||
- [ ] Seal Meat Soup
|
|
||||||
- [ ] Smoked Fish
|
|
||||||
- [ ] Smoked Moose Meat
|
|
||||||
- [ ] Northern Morning Fare
|
|
||||||
- [ ] Oat Flour `[INFERENCE]`
|
|
||||||
- [ ] Oven Pancake Batter
|
|
||||||
- [ ] Raw Kale Chips `[INFERENCE]`
|
|
||||||
- [ ] Unbaked Poteitr
|
|
||||||
- [ ] Frosty fishing bait
|
|
||||||
|
|
||||||
## Seasonal
|
|
||||||
|
|
||||||
### Buildables
|
|
||||||
|
|
||||||
- [ ] Maypole
|
|
||||||
- [ ] Jack-o-turnip
|
|
||||||
- [ ] Mistletoe
|
|
||||||
- [ ] Yule Tree
|
|
||||||
- [ ] Yule Wreath
|
|
||||||
- [ ] Yule Garland
|
|
||||||
- [ ] Yuleklapp — small
|
|
||||||
- [ ] Yuleklapp — medium
|
|
||||||
- [ ] Yuleklapp — large
|
|
||||||
|
|
||||||
### Equipment and inventory
|
|
||||||
|
|
||||||
#### Armor
|
|
||||||
|
|
||||||
- [ ] Midsummer Crown
|
|
||||||
- [ ] Pointy Hat
|
|
||||||
|
|
||||||
## Sources
|
|
||||||
|
|
||||||
The Valheim Wiki gives most current lists and progression tables.
|
|
||||||
Deep North names use the 1.0 notes and game-derived catalogs when Wiki pages lag release data.
|
|
||||||
|
|
||||||
- https://valheim.wiki/w/Weapons
|
|
||||||
- https://valheim.wiki/w/Armor
|
|
||||||
- https://valheim.wiki/w/Tools
|
|
||||||
- https://valheim.wiki/w/Accessories
|
|
||||||
- https://valheim.wiki/w/Trinkets
|
|
||||||
- https://valheim.wiki/w/Food
|
|
||||||
- https://valheim.wiki/w/Mead
|
|
||||||
- https://valheim.wiki/w/Bait
|
|
||||||
- https://valheim.wiki/w/Building
|
|
||||||
- https://valheim.wiki/w/Deep_North
|
|
||||||
- https://www.valheimgame.com/news/valheim-1-0-has-arrived-/
|
|
||||||
- https://www.valheimplanner.com/recipes/
|
|
||||||
- https://www.valheimplanner.com/build/
|
|
||||||
- https://www.valheimplanner.com/food/
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
---
|
|
||||||
aliases:
|
|
||||||
- Credits
|
|
||||||
date-created: 2026-10-17 18:08
|
|
||||||
date-modified: 2026-10-17 23:33
|
|
||||||
---
|
|
||||||
|
|
||||||
# Credits
|
|
||||||
|
|
||||||
| Path | Original | Notes | |
|
|
||||||
|-------------------------------------|-----------------------------------------------------------------------------------------|-------|---|
|
|
||||||
| `//src/skills/agent-writing-skills` | <https://github.com/mattpocock/skills/tree/main/skills/productivity/writing-for-agents> | | |
|
|
||||||
| `//src/skills/retrospect` | <https://github.com/mattpocock/skills/tree/main/skills/engineering/retro> | | |
|
|
||||||
| | | | |
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
aliases:
|
|
||||||
- Dotfiles
|
|
||||||
date-created: 2026-10-14 16:27
|
|
||||||
date-modified: 2026-10-14 16:28
|
|
||||||
---
|
|
||||||
|
|
||||||
# Dotfiles
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
aliases:
|
|
||||||
- Monorepo
|
|
||||||
date-created: 2026-10-14 22:51
|
|
||||||
date-modified: 2026-10-14 22:54
|
|
||||||
---
|
|
||||||
|
|
||||||
# Monorepo
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
aliases:
|
|
||||||
- Productivity
|
|
||||||
date-created: 2026-10-14 16:32
|
|
||||||
date-modified: 2026-10-14 16:33
|
|
||||||
---
|
|
||||||
|
|
||||||
# Productivity
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
aliases:
|
|
||||||
- "0001"
|
|
||||||
- TODO CLI
|
|
||||||
date-created: 2026-10-14 00:49
|
|
||||||
date-modified: 2026-10-14 22:52
|
|
||||||
status: open
|
|
||||||
area: "[[productivity]]"
|
|
||||||
blocked-by:
|
|
||||||
- "[[0005-rust-cli-scaffolding]]"
|
|
||||||
subtasks: []
|
|
||||||
---
|
|
||||||
|
|
||||||
# TODO CLI
|
|
||||||
|
|
||||||
Write a TODO CLI that enforces the task workflow in this vault deterministically. This allows agents to participate in task management with minimal cost instead of relying on markdown generation and parsing arbitrary metadata while maintaining human readability.
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
---
|
|
||||||
aliases:
|
|
||||||
- "0002"
|
|
||||||
- NixOS Refactor
|
|
||||||
date-created: 2026-10-14 16:07
|
|
||||||
date-modified: 2026-10-19 16:44
|
|
||||||
status: closed
|
|
||||||
area: "[[dotfiles]]"
|
|
||||||
blocked-by: []
|
|
||||||
subtasks: []
|
|
||||||
---
|
|
||||||
|
|
||||||
# NixOS Refactor
|
|
||||||
|
|
||||||
Complete the remaining Nix refactor work.
|
|
||||||
|
|
||||||
## Files
|
|
||||||
|
|
||||||
- [x] `flake.nix`
|
|
||||||
- [x] `src/flake/default.nix`
|
|
||||||
- [x] `src/flake/formatter.nix`
|
|
||||||
- [x] `src/flake/apps.nix`
|
|
||||||
- [x] `src/dotfiles/default.nix`
|
|
||||||
- [x] `src/dotfiles/util.nix`
|
|
||||||
- [x] `src/dotfiles/registry.nix`
|
|
||||||
- [x] `src/dotfiles/registry/schema.nix`
|
|
||||||
- [x] `src/dotfiles/registry/validation.nix`
|
|
||||||
- [x] `configs/nixos/registry/address.nix`
|
|
||||||
- [>] `configs/nixos/registry/homepage.nix`
|
|
||||||
- [x] `configs/nixos/registry/route.nix`
|
|
||||||
- [x] `configs/nixos/registry/shell-shortcut.nix`
|
|
||||||
- [x] `configs/nixos/registry/stable-uid.nix`
|
|
||||||
- [x] `configs/nixos/modules/default.nix`
|
|
||||||
- [x] `configs/nixos/modules/host.nix`
|
|
||||||
- [x] `configs/nixos/modules/guest.nix`
|
|
||||||
- [x] `configs/nixos/modules/monorepo.nix`
|
|
||||||
- [x] `configs/nixos/modules/zfs.nix`
|
|
||||||
- [x] `configs/nixos/modules/network/ssh.nix`
|
|
||||||
- [x] `configs/nixos/modules/network/vlans.nix`
|
|
||||||
- [x] `configs/nixos/modules/network/vpn-client.nix`
|
|
||||||
- [x] `configs/nixos/modules/desktop/default.nix`
|
|
||||||
- [x] `configs/nixos/modules/desktop/apps/alacritty.nix`
|
|
||||||
- [x] `configs/nixos/modules/desktop/apps/firefox.nix`
|
|
||||||
- [x] `configs/nixos/modules/desktop/apps/ghostty.nix`
|
|
||||||
- [x] `configs/nixos/modules/desktop/apps/obsidian.nix`
|
|
||||||
- [x] `configs/nixos/modules/desktop/apps/r2modman.nix`
|
|
||||||
- [x] `configs/nixos/modules/desktop/apps/signal.nix`
|
|
||||||
- [x] `configs/nixos/modules/desktop/apps/steam.nix`
|
|
||||||
- [x] `configs/nixos/modules/desktop/apps/vencord.nix`
|
|
||||||
- [x] `configs/nixos/modules/desktop/session/hyprland/default.nix`
|
|
||||||
- [x] `configs/nixos/modules/desktop/session/hyprland/hypridle.nix`
|
|
||||||
- [x] `configs/nixos/modules/desktop/session/hyprland/hyprlock.nix`
|
|
||||||
- [x] `configs/nixos/modules/desktop/session/login.nix`
|
|
||||||
- [x] `configs/nixos/modules/desktop/session/mako.nix`
|
|
||||||
- [x] `configs/nixos/modules/desktop/session/portals.nix`
|
|
||||||
- [x] `configs/nixos/modules/desktop/session/rofi.nix`
|
|
||||||
- [x] `configs/nixos/modules/desktop/session/waybar.nix`
|
|
||||||
- [x] `configs/nixos/modules/desktop/utilities/audio.nix`
|
|
||||||
- [x] `configs/nixos/modules/desktop/utilities/clipboard.nix`
|
|
||||||
- [x] `configs/nixos/modules/desktop/utilities/osd.nix`
|
|
||||||
- [x] `configs/nixos/modules/desktop/utilities/recording.nix`
|
|
||||||
- [x] `configs/nixos/modules/desktop/utilities/screenshot.nix`
|
|
||||||
- [x] `configs/nixos/modules/desktop/utilities/theming.nix`
|
|
||||||
- [x] `configs/nixos/modules/desktop/utilities/userdirs.nix`
|
|
||||||
- [x] `configs/nixos/modules/services/actualbudget.nix`
|
|
||||||
- [x] `configs/nixos/modules/services/copyparty.nix`
|
|
||||||
- [x] `configs/nixos/modules/services/homepage.nix`
|
|
||||||
- [x] `configs/nixos/modules/services/navidrome.nix`
|
|
||||||
- [x] `configs/nixos/modules/services/reverse-proxy.nix`
|
|
||||||
- [x] `configs/nixos/modules/services/steamcmd.nix`
|
|
||||||
- [x] `configs/nixos/modules/services/games/palworld.nix`
|
|
||||||
- [x] `configs/nixos/modules/services/games/valheim.nix`
|
|
||||||
- [x] `configs/nixos/modules/shell/bazel.nix`
|
|
||||||
- [x] `configs/nixos/modules/shell/direnv/default.nix`
|
|
||||||
- [x] `configs/nixos/modules/shell/fish/default.nix`
|
|
||||||
- [x] `configs/nixos/modules/shell/gitea-axi.nix`
|
|
||||||
- [x] `configs/nixos/modules/shell/git.nix`
|
|
||||||
- [x] `configs/nixos/modules/shell/herdr.nix`
|
|
||||||
- [x] `configs/nixos/modules/shell/nvim/default.nix`
|
|
||||||
- [x] `configs/nixos/modules/shell/omp.nix`
|
|
||||||
- [x] `configs/nixos/modules/shell/pi/default.nix`
|
|
||||||
- [x] `configs/nixos/modules/shell/python.nix`
|
|
||||||
- [x] `configs/nixos/modules/shell/rust.nix`
|
|
||||||
- [x] `configs/nixos/modules/shell/tmux/default.nix`
|
|
||||||
- [x] `configs/nixos/modules/shell/yazi.nix`
|
|
||||||
- [x] `src/dotfiles/guests.nix`
|
|
||||||
- [x] `configs/nixos/guests/actualbudget.nix`
|
|
||||||
- [x] `configs/nixos/guests/copyparty.nix`
|
|
||||||
- [x] `configs/nixos/guests/homepage.nix`
|
|
||||||
- [x] `configs/nixos/guests/navidrome.nix`
|
|
||||||
- [x] `configs/nixos/guests/palworld.nix`
|
|
||||||
- [x] `configs/nixos/guests/reverse-proxy.nix`
|
|
||||||
- [x] `configs/nixos/guests/valheim.nix`
|
|
||||||
- [x] `src/dotfiles/hosts.nix`
|
|
||||||
- [x] `configs/nixos/hosts/neogaia/default.nix`
|
|
||||||
- [x] `configs/nixos/hosts/neogaia/disk.nix`
|
|
||||||
- [x] `configs/nixos/hosts/neogaia/hardware-configuration.nix`
|
|
||||||
- [x] `configs/nixos/hosts/pikachu/default.nix`
|
|
||||||
- [x] `configs/nixos/hosts/pikachu/disk.nix`
|
|
||||||
- [x] `configs/nixos/hosts/pikachu/hardware-configuration.nix`
|
|
||||||
- [x] `configs/nixos/hosts/zeus/default.nix`
|
|
||||||
- [x] `configs/nixos/hosts/zeus/disk.nix`
|
|
||||||
- [x] `configs/nixos/hosts/zeus/hardware-configuration.nix`
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
---
|
|
||||||
aliases:
|
|
||||||
- "0003"
|
|
||||||
- Whisper dictation
|
|
||||||
date-created: 2026-10-14 16:31
|
|
||||||
date-modified: 2026-10-14 16:33
|
|
||||||
status: open
|
|
||||||
area: "[[productivity]]"
|
|
||||||
blocked-by:
|
|
||||||
- "[[0002-nixos-refactor]]"
|
|
||||||
subtasks: []
|
|
||||||
---
|
|
||||||
|
|
||||||
# Whisper dictation
|
|
||||||
|
|
||||||
Provide reliable local dictation on Zeus.
|
|
||||||
|
|
||||||
## Prototype findings
|
|
||||||
|
|
||||||
- `hyprwhspr-rs` provides PipeWire capture, local `whisper.cpp` transcription, clipboard copying, and Hyprland integration.
|
|
||||||
- The prototype used `small.en` from `~/dwn/ggml-small.en.bin`.
|
|
||||||
- The initial CPU prototype transcribed accurately but added about 2.3 seconds after a 2.9-second recording.
|
|
||||||
- Vulkan acceleration uses `whisper-cpp-vulkan` and `gpu_layers = 999`.
|
|
||||||
- `SUPER + ALT + D` starts recording on press and stops recording on release.
|
|
||||||
- Hyprland Lua represents release bindings with `hl.bind(..., { release = true })`.
|
|
||||||
- `hyprwhspr-rs` uses Hyprland's removed `sendshortcut` dispatcher for automatic paste.
|
|
||||||
- Its virtual-keyboard fallback can drop modifiers and insert only `v`.
|
|
||||||
- Treat the prototype configuration as disposable because it no longer works reliably.
|
|
||||||
|
|
||||||
## Completion
|
|
||||||
|
|
||||||
- [ ] Choose a maintained local speech-to-text backend.
|
|
||||||
- [ ] Record microphone audio through PipeWire.
|
|
||||||
- [ ] Transcribe locally with GPU acceleration when available.
|
|
||||||
- [ ] Insert transcribed text reliably into the focused application.
|
|
||||||
- [ ] Copy transcribed text to the clipboard.
|
|
||||||
- [ ] Test dictation with representative technical speech.
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
---
|
|
||||||
aliases:
|
|
||||||
- "0004"
|
|
||||||
- Dictionary CLI
|
|
||||||
date-created: 2026-10-14 22:47
|
|
||||||
date-modified: 2026-10-14 22:57
|
|
||||||
status: open
|
|
||||||
area: "[[productivity]]"
|
|
||||||
blocked-by:
|
|
||||||
- "[[0005-rust-cli-scaffolding]]"
|
|
||||||
subtasks: []
|
|
||||||
---
|
|
||||||
|
|
||||||
# Dictionary CLI
|
|
||||||
|
|
||||||
A CLI tool that basically works as a strict allowlist for words used in prose such as markdown files or strings passed to the tool.
|
|
||||||
|
|
||||||
The basic idea can be found in [[dictionary]], but as it would be a deterministic tool, a strict allowlist might be more beneficial.
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
---
|
|
||||||
aliases:
|
|
||||||
- "0005"
|
|
||||||
- Rust CLI Scaffolding
|
|
||||||
date-created: 2026-10-14 22:50
|
|
||||||
date-modified: 2026-10-15 23:48
|
|
||||||
# Allowed: open, closed
|
|
||||||
status: open
|
|
||||||
area: "[[monorepo]]"
|
|
||||||
blocked-by:
|
|
||||||
- "[[0009-bazel-project-scaffolding]]"
|
|
||||||
subtasks: []
|
|
||||||
---
|
|
||||||
|
|
||||||
# Rust CLI Scaffolding
|
|
||||||
|
|
||||||
One of the biggest patterns I noticed when working on Rust projects in the monorepo was that they were mostly CLI tools. I found myself doing the same boilerplate over and over again.
|
|
||||||
|
|
||||||
I think much like Google, having a function that handles sensible defaults would be a good idea.
|
|
||||||
|
|
||||||
But also, maybe a Rust project set up to avoid the common pitfalls?
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
aliases:
|
|
||||||
- "0006"
|
|
||||||
- Wallpaper Randomizer
|
|
||||||
date-created: 2026-10-14 23:00
|
|
||||||
date-modified: 2026-10-14 23:05
|
|
||||||
status: open
|
|
||||||
area: "[[dotfiles]]"
|
|
||||||
blocked-by:
|
|
||||||
- "[[0005-rust-cli-scaffolding]]"
|
|
||||||
subtasks: []
|
|
||||||
---
|
|
||||||
|
|
||||||
# Wallpaper Randomizer
|
|
||||||
|
|
||||||
This is kind of two ideas in one:
|
|
||||||
|
|
||||||
1. Randomizing the desktop wallpaper from a select list.
|
|
||||||
2. A web service endpoint that provides a random image from a configured directory/set.
|
|
||||||
|
|
||||||
The first idea might not need any special code, so it wouldn't be a CLI.
|
|
||||||
|
|
||||||
I think that any tools I write should have both a non-interactive mode and a TUI or web server mode.
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
aliases:
|
|
||||||
- "0007"
|
|
||||||
- Obsidian Source Notes
|
|
||||||
date-created: 2026-10-14 23:11
|
|
||||||
date-modified: 2026-10-14 23:14
|
|
||||||
status: open
|
|
||||||
area: "[[productivity]]"
|
|
||||||
blocked-by: []
|
|
||||||
subtasks: []
|
|
||||||
---
|
|
||||||
|
|
||||||
# Obsidian Source Notes
|
|
||||||
|
|
||||||
This is more of a note to collect ideas for `ingest-*` skills.
|
|
||||||
|
|
||||||
## Skill Ideas
|
|
||||||
|
|
||||||
- `ingest-youtube-transcript`
|
|
||||||
- `ingest-youtube-video`
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
---
|
|
||||||
aliases:
|
|
||||||
- "0008"
|
|
||||||
- Vim Obsidian Markdown Renderer
|
|
||||||
date-created: 2026-10-14 23:18
|
|
||||||
date-modified: 2026-10-14 23:20
|
|
||||||
status: open
|
|
||||||
area: "[[dotfiles]]"
|
|
||||||
blocked-by: []
|
|
||||||
subtasks: []
|
|
||||||
---
|
|
||||||
|
|
||||||
# Vim Obsidian Markdown Renderer
|
|
||||||
|
|
||||||
The current plugin I use for Markdown rendering in Vim has some issues, but most importantly it doesn't have full feature parity with Obsidian. I think from the top of my head, wikilinks can't be followed for example.
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user