Compare commits
49 Commits
85fadc5bfc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| dcc4efcc75 | |||
| a23a317335 | |||
| 6e545c0a15 | |||
| 6081e3e325 | |||
| 719370c8f7 | |||
| 2b4e66a75c | |||
| c44769ff70 | |||
| 1397576538 | |||
| 5fbe1850e0 | |||
| 57bb21af1a | |||
| ffc40e547b | |||
| 55f61f71d8 | |||
| 08563c6769 | |||
| 8462b3a5a8 | |||
| d0872e7d8c | |||
| 9be9ad7675 | |||
| 5ee328cdf8 | |||
| 6532a822cd | |||
| 20a3990979 | |||
| b7ab518f58 | |||
| 3f1e34ef95 | |||
| d0abfc1625 | |||
| 031090e4ea | |||
| 2edad6c5df | |||
| 1d5acc09c6 | |||
| ee29e53f7c | |||
| 3c9d0ec886 | |||
| f0c5544b10 | |||
| 4dd0a5a4f5 | |||
| a4a795f7ee | |||
| ecdf877308 | |||
| d1face10ec | |||
| 337d9a2014 | |||
| f059068873 | |||
| 7662c04cda | |||
| 33128fc3c7 | |||
| 68415ad47e | |||
| 6b7201e6fa | |||
| 4360f4863b | |||
| 370cab8f83 | |||
| e74a08b853 | |||
| 250b0b71f9 | |||
| 10299cec19 | |||
| 91654b3068 | |||
| be9c3c07a3 | |||
| a8bd4bf4cf | |||
| a2fc568704 | |||
| 0bf5ebae87 | |||
| fddec14128 |
@@ -17,15 +17,6 @@ If the target includes multiple matching file types, use all matching skills.
|
||||
If the target is a diff, choose skills from the changed files.
|
||||
Do not use a style skill when no reviewed file matches it.
|
||||
|
||||
For every reviewed `*.nix` file, run:
|
||||
|
||||
```bash
|
||||
bazel run //src/natu:natu -- <reviewed-nix-paths>
|
||||
```
|
||||
|
||||
Pass every reviewed Nix path to Natu.
|
||||
Report every Natu diagnostic as an actionable finding.
|
||||
|
||||
Report only actionable findings.
|
||||
Order findings by severity.
|
||||
Do not report preferences unless a loaded rule states them.
|
||||
@@ -45,7 +36,6 @@ Keep the code excerpt to one to three lines when possible.
|
||||
Use a longer excerpt only when the violation needs surrounding context.
|
||||
Use `...` to omit unrelated lines from a multi-line excerpt.
|
||||
Use a style-skill rule citation in the form `skill::rule-file:line`.
|
||||
Cite Natu diagnostics as `natu::<rule-id>`.
|
||||
Make the fix direct.
|
||||
Do not include alternatives when one fix best follows the cited rule.
|
||||
|
||||
|
||||
1
.pi/skills/conventional-commits
Symbolic link
1
.pi/skills/conventional-commits
Symbolic link
@@ -0,0 +1 @@
|
||||
../../src/skills/conventional-commits
|
||||
@@ -3,7 +3,6 @@
|
||||
## Commit convention
|
||||
|
||||
- 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.
|
||||
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.
|
||||
@@ -105,13 +104,10 @@
|
||||
Do not use `nix fmt` as a validation command until a formatter is added.
|
||||
- The `fix` app passes its current directory to Deadnix.
|
||||
Run `nix run .#fix` from the repository root so path exclusions match.
|
||||
- During the Nix refactor, address each issue that comes up by adding either a Natu rule or a new Nix style rule.
|
||||
- Git pathspec searches started in a subdirectory default to that subtree.
|
||||
Use `git -C <repository-root>` or top-anchored pathspecs for repository-wide history searches.
|
||||
- A `**/*.nix` Git pathspec does not include Nix files directly in its starting directory.
|
||||
Include both `*.nix` and `**/*.nix`, or use a recursive `*.nix` pathspec, when linting every Nix file.
|
||||
- Nix `let` bindings are recursive; their source order does not determine binding availability.
|
||||
Treat Natu `let_order` as a readability rule, not an evaluation dependency.
|
||||
- Ariadne extraction must preserve the game-root season prefix for archived source.
|
||||
Different game installs can contain compiled scripts with the same internal path.
|
||||
- Clew owns Ren'Py extraction, preparation, and compiled-script AST export. Ariadne owns analysis over Clew-produced data.
|
||||
@@ -122,7 +118,9 @@
|
||||
- OMP does not discover project skills from `.pi/skills`.
|
||||
Put skills shared by Pi and OMP under `.agents/skills`, or configure OMP's `skills.customDirectories`.
|
||||
- Pi is vendored under `third_party/pi` as a linear vendor import.
|
||||
`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.
|
||||
Gitea rebases the synthetic upstream root commit without the `third_party/pi` prefix.
|
||||
Preserve `git-subtree-dir` and `git-subtree-split` trailers on Pi vendor commits.
|
||||
- The default Pi model for NixOS user configuration is set in `configs/nixos/modules/shell/pi/default.nix`, not in vendored Pi source.
|
||||
- The `monorepo` and `status` libraries do not have `README.md` files.
|
||||
|
||||
154
Cargo.lock
generated
154
Cargo.lock
generated
@@ -2,15 +2,6 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
@@ -71,12 +62,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.7"
|
||||
@@ -108,7 +93,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -123,41 +108,6 @@ version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "countme"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
|
||||
|
||||
[[package]]
|
||||
name = "googletest"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6b5e2f2b556b7b90297a5a35c8267dd43a537923d2b329beefdba2b4ec19d94"
|
||||
dependencies = [
|
||||
"googletest_macro",
|
||||
"num-traits",
|
||||
"regex",
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "googletest_macro"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ae6abc96141edd26bf5aeec0f119c129c44de3ced09e5073711a02cb74725d0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
@@ -190,27 +140,6 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
name = "monorepo"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "natu"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"googletest",
|
||||
"monorepo",
|
||||
"rnix",
|
||||
"rowan",
|
||||
"status",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
@@ -235,68 +164,6 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "rnix"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c163bd17372eecdf10d351c34584b7de7c1a33be4e92a32f3fb3f5a7fe3f579b"
|
||||
dependencies = [
|
||||
"rowan",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rowan"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21"
|
||||
dependencies = [
|
||||
"countme",
|
||||
"hashbrown",
|
||||
"rustc-hash",
|
||||
"text-size",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
@@ -324,7 +191,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -350,17 +217,6 @@ version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.6"
|
||||
@@ -372,12 +228,6 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "text-size"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.26"
|
||||
|
||||
@@ -4,16 +4,12 @@ members = [
|
||||
"src/machoke",
|
||||
"src/monorepo",
|
||||
"src/status",
|
||||
"src/natu",
|
||||
]
|
||||
resolver = "3"
|
||||
|
||||
[workspace.dependencies]
|
||||
clap = "4"
|
||||
googletest = "0.14"
|
||||
monorepo = { path = "src/monorepo" }
|
||||
rnix = "=0.14.0"
|
||||
rowan = "0.16"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
status = { path = "src/status" }
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
## Desktop
|
||||
|
||||
- TODO: Verify this path after the NixOS configuration is migrated.
|
||||
When adding a graphical application, add a `window-rewrite` icon mapping in `modules/desktop/session/waybar.nix`.
|
||||
- When adding a graphical application, register its workspace icon in `registry.window-icon`.
|
||||
The workspace indicator uses this mapping to show an application icon instead of the generic default glyph.
|
||||
Match the mapping on the window class.
|
||||
Store the icon as the rendered string.
|
||||
Run `hyprctl clients -j | jq -r '.[].class' | sort -u` to list classes in the running session.
|
||||
- TODO: Verify this path after the NixOS configuration is migrated.
|
||||
Set Hyprland session environment variables in `modules/desktop/session/hyprland/default.nix` with `env = KEY,VALUE`.
|
||||
@@ -65,3 +65,6 @@
|
||||
and Zeus), not in the generic host module.
|
||||
- Module-relative paths resolve from the module file. Update them when moving a module into a subdirectory.
|
||||
- On Pikachu, `uptime` does not support `-s`. Use `who -b` to report the boot time.
|
||||
- Evaluating a NixOS guest's inner `config.services.nginx` can force incomplete ACME values.
|
||||
Evaluate the parent host's `config.system.build.toplevel` to validate an ACME-enabled guest.
|
||||
- `pkgs.systemd` does not set `meta.mainProgram`. Use `lib.getExe' pkgs.systemd "systemctl"` instead of `lib.getExe pkgs.systemd`.
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.guests.actualbudget; # NOLINT(let_order)
|
||||
dataDir = "/data";
|
||||
in
|
||||
{
|
||||
options.guests.actualbudget = {
|
||||
dataPath = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "/srv/actualbudget";
|
||||
description = "Host path bind-mounted as Actual Budget's data directory.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = cfg.dataPath != null;
|
||||
message = "guests.actualbudget.dataPath must point at the host directory that stores Actual Budget data.";
|
||||
}
|
||||
];
|
||||
|
||||
registry.homepage.${config.networking.hostName}.actualbudget = {
|
||||
tab = "Apps";
|
||||
section = "Other";
|
||||
name = "Actual Budget";
|
||||
description = "Finance";
|
||||
icon = "actual-budget.png";
|
||||
};
|
||||
|
||||
guests.actualbudget = {
|
||||
interior = {
|
||||
modules.services.actualbudget = {
|
||||
enable = true;
|
||||
inherit dataDir;
|
||||
};
|
||||
};
|
||||
mounts.${dataDir}.directory = cfg.dataPath;
|
||||
limits = {
|
||||
memory = lib.mkDefault "2G";
|
||||
cpu = lib.mkDefault "200%";
|
||||
tasksMax = lib.mkDefault 512;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -4,58 +4,14 @@
|
||||
...
|
||||
}:
|
||||
let
|
||||
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.";
|
||||
};
|
||||
};
|
||||
};
|
||||
cfg = config.guests.copyparty;
|
||||
in
|
||||
{
|
||||
options.guests.copyparty = {
|
||||
accountName = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "alexion";
|
||||
description = "Copyparty account name configured for password authentication.";
|
||||
};
|
||||
|
||||
passwordSecret = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "services/copyparty/users/alexion/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.";
|
||||
default = "services/copyparty/users/${config.user.name}/password";
|
||||
description = "SOPS secret name that contains the Copyparty account password.";
|
||||
};
|
||||
|
||||
statePath = lib.mkOption {
|
||||
@@ -72,7 +28,39 @@ in
|
||||
};
|
||||
|
||||
volumes = lib.mkOption {
|
||||
type = lib.types.attrsOf volumeType;
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule {
|
||||
options = {
|
||||
hostPath = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Host path bind-mounted into the Copyparty guest for this volume.";
|
||||
};
|
||||
|
||||
path = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Guest path served by this Copyparty volume.";
|
||||
};
|
||||
|
||||
access = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
|
||||
default = { };
|
||||
description = "Copyparty access map from permission string to account names.";
|
||||
};
|
||||
|
||||
readOnly = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = "Serve this Copyparty volume read-only.";
|
||||
};
|
||||
|
||||
flags = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
description = "Copyparty volume flags.";
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
default = { };
|
||||
description = "Copyparty volumes keyed by their URL path.";
|
||||
};
|
||||
@@ -102,24 +90,26 @@ in
|
||||
guests.copyparty = {
|
||||
interior.modules.services.copyparty = {
|
||||
enable = true;
|
||||
inherit (cfg) accountName stateDir;
|
||||
passwordFile = "/run/secrets/${cfg.passwordSecret}";
|
||||
reverseProxy = {
|
||||
enable = cfg.reverseProxy.enable;
|
||||
inherit (cfg.reverseProxy) trustedSources;
|
||||
};
|
||||
volumes = lib.mapAttrs (_urlPath: volume: {
|
||||
inherit (volume)
|
||||
path
|
||||
access
|
||||
flags
|
||||
readOnly
|
||||
;
|
||||
}) cfg.volumes;
|
||||
volumes = lib.mapAttrs (
|
||||
_urlPath: volume:
|
||||
{
|
||||
inherit (volume)
|
||||
path
|
||||
readOnly
|
||||
;
|
||||
}
|
||||
// lib.optionalAttrs (volume.access != { }) { inherit (volume) access; }
|
||||
// lib.optionalAttrs (volume.flags != { }) { inherit (volume) flags; }
|
||||
) cfg.volumes;
|
||||
};
|
||||
|
||||
mounts = {
|
||||
${cfg.stateDir} = {
|
||||
"/var/lib/copyparty" = {
|
||||
directory = cfg.statePath;
|
||||
owner = config.registry."stable-uid".${config.networking.hostName}.copyparty;
|
||||
group = "storage";
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.guests.homepage; # NOLINT(let_order)
|
||||
cfg = config.guests.homepage;
|
||||
environmentFile = "/run/secrets/homepage.env";
|
||||
in
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.guests.palworld; # NOLINT(let_order)
|
||||
cfg = config.guests.palworld;
|
||||
steamDir = "/data/steamcmd";
|
||||
installDir = "/data/server";
|
||||
stateDir = "/data/state";
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.guests.valheim; # NOLINT(let_order)
|
||||
cfg = config.guests.valheim;
|
||||
steamDir = "/data/steamcmd";
|
||||
installDir = "/data/server";
|
||||
stateDir = "/data/state";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
inputs,
|
||||
util,
|
||||
...
|
||||
}:
|
||||
# neogaia — Dell XPS 13 9380 laptop.
|
||||
@@ -14,8 +15,7 @@
|
||||
|
||||
modules.host.cachyos.enable = true;
|
||||
|
||||
# So wifi can be joined from the console.
|
||||
networking.networkmanager.enable = true;
|
||||
modules.network.enable = true;
|
||||
|
||||
registry.address.neogaia.self = "10.23.50.146";
|
||||
|
||||
@@ -40,4 +40,23 @@
|
||||
modules.desktop.apps.obsidian.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,22 +71,6 @@
|
||||
};
|
||||
};
|
||||
|
||||
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 = {
|
||||
enable = true;
|
||||
vlan = 20;
|
||||
@@ -145,35 +129,21 @@
|
||||
"/" = {
|
||||
hostPath = "/pikachu/data/copyparty/root";
|
||||
path = "/srv";
|
||||
access.r = [ "alexion" ];
|
||||
readOnly = true;
|
||||
};
|
||||
"/inbox" = {
|
||||
hostPath = "/pikachu/__inbox";
|
||||
path = "/pikachu/__inbox";
|
||||
access.A = [ "alexion" ];
|
||||
flags = [
|
||||
"d2d"
|
||||
"d2t"
|
||||
"dthumb"
|
||||
];
|
||||
};
|
||||
"/media" = {
|
||||
hostPath = "/pikachu/media";
|
||||
path = "/pikachu/media";
|
||||
access.A = [ "alexion" ];
|
||||
flags = [
|
||||
"d2d"
|
||||
"d2t"
|
||||
"dthumb"
|
||||
];
|
||||
};
|
||||
"/opds" = {
|
||||
hostPath = "/pikachu/data/opds";
|
||||
path = "/pikachu/data/opds";
|
||||
access.r = [ "alexion" ];
|
||||
readOnly = true;
|
||||
flags = [ "opds" ];
|
||||
flags.opds = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
enable32Bit = true;
|
||||
};
|
||||
|
||||
networking.networkmanager.enable = true;
|
||||
modules.network.enable = true;
|
||||
|
||||
services.logind.settings.Login = {
|
||||
HandlePowerKey = "ignore";
|
||||
@@ -52,14 +52,14 @@
|
||||
{
|
||||
output = "HDMI-A-1";
|
||||
mode = "1920x1080@120";
|
||||
position = "0x0";
|
||||
position = "-1080x0";
|
||||
scale = 1;
|
||||
transform = 3;
|
||||
}
|
||||
{
|
||||
output = "DP-3";
|
||||
mode = "2560x1440@239.96";
|
||||
position = "1080x0";
|
||||
position = "0x0";
|
||||
scale = 1;
|
||||
}
|
||||
];
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
inputs,
|
||||
lib,
|
||||
pkgs,
|
||||
util,
|
||||
...
|
||||
}:
|
||||
# Configures Firefox as the desktop browser.
|
||||
let
|
||||
cfg = config.modules.desktop.apps.firefox; # NOLINT(let_order)
|
||||
cfg = config.modules.desktop.apps.firefox;
|
||||
in
|
||||
{
|
||||
options.modules.desktop.apps.firefox = {
|
||||
@@ -26,6 +27,11 @@ in
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
registry.window-icon.${config.networking.hostName}.firefox = {
|
||||
class = ".*[Ff]irefox.*";
|
||||
icon = util.glyph "f269";
|
||||
};
|
||||
|
||||
home-manager.users.${config.user.name} = hm: {
|
||||
programs.firefox = {
|
||||
enable = true;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
util,
|
||||
...
|
||||
}:
|
||||
# Ghostty as the desktop terminal.
|
||||
@@ -12,6 +13,19 @@ in
|
||||
options.modules.desktop.apps.ghostty.enable = lib.mkEnableOption "Ghostty as the desktop terminal";
|
||||
|
||||
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 ];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
util,
|
||||
...
|
||||
}:
|
||||
# Obsidian as the desktop note-taking vault.
|
||||
@@ -14,5 +15,10 @@ in
|
||||
|
||||
config = lib.mkIf config.modules.desktop.apps.obsidian.enable {
|
||||
home-manager.users.${user}.home.packages = [ pkgs.obsidian ];
|
||||
|
||||
registry.window-icon.${config.networking.hostName}.obsidian = {
|
||||
class = "obsidian";
|
||||
icon = util.glyph "f02d";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
util,
|
||||
...
|
||||
}:
|
||||
# Signal desktop messenger.
|
||||
@@ -13,5 +14,10 @@ in
|
||||
|
||||
config = lib.mkIf config.modules.desktop.apps.signal.enable {
|
||||
home-manager.users.${user}.home.packages = [ pkgs.signal-desktop ];
|
||||
|
||||
registry.window-icon.${config.networking.hostName}.signal = {
|
||||
class = "[Ss]ignal";
|
||||
icon = util.glyph "f4ad";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
util,
|
||||
...
|
||||
}:
|
||||
# Steam game launcher and runtime integration.
|
||||
@@ -23,5 +24,10 @@
|
||||
environment.systemPackages = [
|
||||
pkgs.gamescope
|
||||
];
|
||||
|
||||
registry.window-icon.${config.networking.hostName}.steam = {
|
||||
class = "[Ss]team";
|
||||
icon = util.glyph "f1b6";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
util,
|
||||
...
|
||||
}:
|
||||
# Vesktop, a Discord client with Vencord built in.
|
||||
@@ -14,5 +15,10 @@ in
|
||||
|
||||
config = lib.mkIf config.modules.desktop.apps.vencord.enable {
|
||||
home-manager.users.${user}.home.packages = [ pkgs.vesktop ];
|
||||
|
||||
registry.window-icon.${config.networking.hostName}.vesktop = {
|
||||
class = "[Vv]esktop";
|
||||
icon = util.glyph "f1ff";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,5 +23,6 @@
|
||||
modules.desktop.utilities.screenshot.enable = lib.mkDefault true;
|
||||
modules.desktop.utilities.theming.enable = lib.mkDefault true;
|
||||
modules.desktop.utilities.userdirs.enable = lib.mkDefault true;
|
||||
registry.env.${config.networking.hostName}.NIXOS_OZONE_WL = "1";
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
util,
|
||||
...
|
||||
}:
|
||||
# The Hyprland compositor.
|
||||
@@ -8,6 +9,7 @@
|
||||
options.modules.desktop.session.hyprland.enable = lib.mkEnableOption "the Hyprland compositor";
|
||||
|
||||
config = lib.mkIf config.modules.desktop.session.hyprland.enable {
|
||||
|
||||
# This program integration owns the session, portals, and polkit, launched
|
||||
# through the universal Wayland session manager.
|
||||
programs.hyprland = {
|
||||
@@ -32,16 +34,211 @@
|
||||
configType = "lua";
|
||||
|
||||
settings = {
|
||||
config.general.layout = "dwindle";
|
||||
config = {
|
||||
input = {
|
||||
kb_layout = config.services.xserver.xkb.layout;
|
||||
repeat_delay = 250;
|
||||
repeat_rate = 45;
|
||||
accel_profile = "flat";
|
||||
touchpad = {
|
||||
natural_scroll = true;
|
||||
tap_to_click = true;
|
||||
disable_while_typing = true;
|
||||
};
|
||||
};
|
||||
|
||||
bind = [
|
||||
{
|
||||
_args = [
|
||||
"SUPER + Return"
|
||||
(lib.generators.mkLuaInline ''hl.dsp.exec_cmd("ghostty")'')
|
||||
];
|
||||
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: {
|
||||
_args = [
|
||||
name
|
||||
value
|
||||
];
|
||||
}) (lib.attrByPath [ config.networking.hostName ] { } config.registry.env);
|
||||
|
||||
bind = lib.mapAttrsToList (_name: keybind: {
|
||||
_args = [
|
||||
keybind.keys
|
||||
(lib.generators.mkLuaInline (
|
||||
{
|
||||
exec = "hl.dsp.exec_cmd(${lib.generators.toLua { } keybind.action.command})";
|
||||
focus = "hl.dsp.focus(${
|
||||
lib.generators.toLua { } { inherit (keybind.action) direction workspace; }
|
||||
})";
|
||||
"move-window" = "hl.dsp.window.move(${
|
||||
lib.generators.toLua { } { inherit (keybind.action) direction workspace; }
|
||||
})";
|
||||
"resize-window" = "hl.dsp.window.resize(${
|
||||
lib.generators.toLua { } (
|
||||
{
|
||||
down = {
|
||||
x = 0;
|
||||
y = 10;
|
||||
relative = true;
|
||||
};
|
||||
left = {
|
||||
x = -10;
|
||||
y = 0;
|
||||
relative = true;
|
||||
};
|
||||
right = {
|
||||
x = 10;
|
||||
y = 0;
|
||||
relative = true;
|
||||
};
|
||||
up = {
|
||||
x = 0;
|
||||
y = -10;
|
||||
relative = true;
|
||||
};
|
||||
}
|
||||
.${keybind.action.direction}
|
||||
)
|
||||
})";
|
||||
"toggle-floating" = ''hl.dsp.window.float({ action = "toggle" })'';
|
||||
"toggle-fullscreen" = "hl.dsp.window.fullscreen()";
|
||||
"close-window" = "hl.dsp.window.close()";
|
||||
}
|
||||
.${keybind.action.kind}
|
||||
))
|
||||
]
|
||||
++ lib.optional keybind.repeating {
|
||||
repeating = true;
|
||||
}
|
||||
];
|
||||
++ lib.optional keybind.locked {
|
||||
locked = true;
|
||||
};
|
||||
}) (lib.attrByPath [ config.networking.hostName ] { } config.registry.keybind);
|
||||
};
|
||||
};
|
||||
registry.keybind.${config.networking.hostName} = lib.mkMerge [
|
||||
# Generate numeric workspace keybinds.
|
||||
(lib.listToAttrs (
|
||||
lib.concatMap (workspace: [
|
||||
{
|
||||
name = "hyprland-workspace-${toString workspace}";
|
||||
value = {
|
||||
keys = "SUPER + ${toString workspace}";
|
||||
action = {
|
||||
kind = "focus";
|
||||
inherit workspace;
|
||||
};
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "hyprland-move-to-workspace-${toString workspace}";
|
||||
value = {
|
||||
keys = "SUPER + SHIFT + ${toString workspace}";
|
||||
action = {
|
||||
kind = "move-window";
|
||||
inherit workspace;
|
||||
};
|
||||
};
|
||||
}
|
||||
]) (lib.range 1 9)
|
||||
))
|
||||
|
||||
# Generate directional keybinds.
|
||||
(lib.listToAttrs (
|
||||
lib.concatMap
|
||||
(
|
||||
{ direction, key }:
|
||||
[
|
||||
{
|
||||
name = "hyprland-focus-${direction}";
|
||||
value = {
|
||||
keys = "SUPER + ${key}";
|
||||
action = {
|
||||
kind = "focus";
|
||||
inherit direction;
|
||||
};
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "hyprland-move-${direction}";
|
||||
value = {
|
||||
keys = "SUPER + SHIFT + ${key}";
|
||||
action = {
|
||||
kind = "move-window";
|
||||
inherit direction;
|
||||
};
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "hyprland-resize-${direction}";
|
||||
value = {
|
||||
keys = "SUPER + ALT + ${key}";
|
||||
repeating = true;
|
||||
action = {
|
||||
kind = "resize-window";
|
||||
inherit direction;
|
||||
};
|
||||
};
|
||||
}
|
||||
]
|
||||
)
|
||||
[
|
||||
{
|
||||
key = "H";
|
||||
direction = "left";
|
||||
}
|
||||
{
|
||||
key = "J";
|
||||
direction = "down";
|
||||
}
|
||||
{
|
||||
key = "K";
|
||||
direction = "up";
|
||||
}
|
||||
{
|
||||
key = "L";
|
||||
direction = "right";
|
||||
}
|
||||
]
|
||||
))
|
||||
{
|
||||
hyprland-toggle-floating = {
|
||||
keys = "SUPER + Space";
|
||||
action.kind = "toggle-floating";
|
||||
};
|
||||
|
||||
hyprland-toggle-fullscreen = {
|
||||
keys = "SUPER + F";
|
||||
action.kind = "toggle-fullscreen";
|
||||
};
|
||||
|
||||
hyprland-close-window = {
|
||||
keys = "SUPER + SHIFT + Q";
|
||||
action.kind = "close-window";
|
||||
};
|
||||
}
|
||||
];
|
||||
|
||||
registry.waybar.${config.networking.hostName}."hyprland/workspaces" = {
|
||||
position = "left";
|
||||
order = 10;
|
||||
settings = {
|
||||
format = "{id} {windows}";
|
||||
format-window-separator = " ";
|
||||
window-rewrite-default = util.glyph "f2d0";
|
||||
window-rewrite = lib.mapAttrs' (
|
||||
_name: windowIcon: lib.nameValuePair "class<${windowIcon.class}>" windowIcon.icon
|
||||
) (lib.attrByPath [ config.networking.hostName ] { } config.registry.window-icon);
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,50 +5,32 @@
|
||||
...
|
||||
}:
|
||||
# hypridle: idle-triggered locking and display power management.
|
||||
let
|
||||
cfg = config.modules.desktop.session.hyprland.hypridle; # NOLINT(let_order)
|
||||
|
||||
# The guard drops the call when a locker is already up, so no idle trigger
|
||||
# stacks a second hyprlock over the first.
|
||||
lockCmd = "${pkgs.procps}/bin/pidof hyprlock || ${hyprlock}";
|
||||
user = config.user.name;
|
||||
|
||||
hyprctl = "${config.programs.hyprland.package}/bin/hyprctl";
|
||||
hyprlock = "${config.home-manager.users.${user}.programs.hyprlock.package}/bin/hyprlock";
|
||||
|
||||
in
|
||||
{
|
||||
options.modules.desktop.session.hyprland.hypridle = {
|
||||
enable = lib.mkEnableOption "hypridle idle management";
|
||||
options.modules.desktop.session.hyprland.hypridle.enable =
|
||||
lib.mkEnableOption "hypridle idle management";
|
||||
|
||||
lockTimeout = lib.mkOption {
|
||||
type = lib.types.ints.positive;
|
||||
default = 300;
|
||||
description = "Seconds of inactivity before the screen locks.";
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
config = lib.mkIf config.modules.desktop.session.hyprland.hypridle.enable {
|
||||
# Closing the lid suspends, and every suspend locks first through the
|
||||
# before_sleep_cmd below, so the lid always lands at a locked screen.
|
||||
services.logind.settings.Login.HandleLidSwitch = "suspend";
|
||||
|
||||
home-manager.users.${user}.services.hypridle = {
|
||||
home-manager.users.${config.user.name}.services.hypridle = {
|
||||
enable = true;
|
||||
|
||||
settings = {
|
||||
general = {
|
||||
lock_cmd = lockCmd;
|
||||
# The guard drops the call when a locker is already up, so no idle
|
||||
# trigger stacks a second hyprlock over the first.
|
||||
lock_cmd = "${lib.getExe' pkgs.procps "pidof"} hyprlock || ${lib.getExe pkgs.hyprlock}";
|
||||
before_sleep_cmd = "loginctl lock-session";
|
||||
# Waking pokes the compositor in case firmware leaves displays dark.
|
||||
after_sleep_cmd = "${hyprctl} dispatch dpms on";
|
||||
after_sleep_cmd = "${lib.getExe' config.programs.hyprland.package "hyprctl"} dispatch 'hl.dsp.dpms({ action = \"enable\" })'";
|
||||
};
|
||||
|
||||
listener = [
|
||||
# Lock on idle.
|
||||
{
|
||||
timeout = cfg.lockTimeout;
|
||||
timeout = 300;
|
||||
on-timeout = "loginctl lock-session";
|
||||
}
|
||||
];
|
||||
|
||||
@@ -2,44 +2,54 @@
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
util,
|
||||
...
|
||||
}:
|
||||
# The lock screen: a hyprlock session-lock surface the compositor owns.
|
||||
let
|
||||
user = config.user.name;
|
||||
|
||||
# The hyprlock this module installs, used by the lock keybind below.
|
||||
hyprlock = "${config.home-manager.users.${user}.programs.hyprlock.package}/bin/hyprlock";
|
||||
|
||||
sleepIcon = builtins.fromJSON ''"\udb81\udcb2"'';
|
||||
sleepOffIcon = builtins.fromJSON ''"\udb81\udcb3"'';
|
||||
sleepIcon = util.glyph [
|
||||
"db81"
|
||||
"dcb2"
|
||||
];
|
||||
sleepOffIcon = util.glyph [
|
||||
"db81"
|
||||
"dcb3"
|
||||
];
|
||||
|
||||
idleLockToggle = pkgs.writeShellScript "lockscreen-idle-inhibit-toggle" ''
|
||||
unit=lockscreen-idle-inhibit.service
|
||||
|
||||
refresh_waybar() {
|
||||
${pkgs.procps}/bin/pkill -RTMIN+9 waybar >/dev/null 2>&1 || true
|
||||
${lib.getExe' pkgs.procps "pkill"} -RTMIN+9 waybar >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
if ${pkgs.systemd}/bin/systemctl --user is-active --quiet "$unit"; then
|
||||
${pkgs.systemd}/bin/systemctl --user stop "$unit"
|
||||
if ${lib.getExe' pkgs.systemd "systemctl"} --user is-active --quiet "$unit"; then
|
||||
${lib.getExe' pkgs.systemd "systemctl"} --user stop "$unit"
|
||||
refresh_waybar
|
||||
${pkgs.swayosd}/bin/swayosd-client --custom-message "${sleepIcon}"
|
||||
${lib.getExe' pkgs.swayosd "swayosd-client"} --custom-message "${sleepIcon}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ${pkgs.systemd}/bin/systemd-run \
|
||||
if ${lib.getExe' pkgs.systemd "systemd-run"} \
|
||||
--user \
|
||||
--collect \
|
||||
--unit=lockscreen-idle-inhibit \
|
||||
--description="Disable idle-triggered screen locking" \
|
||||
${pkgs.systemd}/bin/systemd-inhibit \
|
||||
${lib.getExe' pkgs.systemd "systemd-inhibit"} \
|
||||
--what=idle \
|
||||
--who=lockscreen-hotkey \
|
||||
--why="lock screen disabled by hotkey" \
|
||||
${pkgs.coreutils}/bin/sleep infinity >/dev/null; then
|
||||
${lib.getExe' pkgs.coreutils "sleep"} infinity >/dev/null; then
|
||||
refresh_waybar
|
||||
${pkgs.swayosd}/bin/swayosd-client --custom-message "${sleepOffIcon}"
|
||||
${lib.getExe' pkgs.swayosd "swayosd-client"} --custom-message "${sleepOffIcon}"
|
||||
fi
|
||||
'';
|
||||
|
||||
idleLockStatus = pkgs.writeShellScript "waybar-idle-lock-status" ''
|
||||
if ${lib.getExe' pkgs.systemd "systemctl"} --user is-active --quiet lockscreen-idle-inhibit.service; then
|
||||
printf '{"text":"${sleepOffIcon}","tooltip":"Idle locking inhibited","class":"inhibited"}\n'
|
||||
else
|
||||
printf '{"text":"","class":"idle"}\n'
|
||||
fi
|
||||
'';
|
||||
in
|
||||
@@ -48,7 +58,7 @@ in
|
||||
lib.mkEnableOption "the hyprlock lock screen";
|
||||
|
||||
config = lib.mkIf config.modules.desktop.session.hyprland.hyprlock.enable {
|
||||
home-manager.users.${user} = {
|
||||
home-manager.users.${config.user.name} = {
|
||||
# Colors and the lock-screen background come from Stylix's hyprlock target,
|
||||
# so only geometry and behaviour are set here.
|
||||
programs.hyprlock = {
|
||||
@@ -81,16 +91,36 @@ in
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Lock on Super+X.
|
||||
# The guard drops the keypress when a locker is already up, so a second
|
||||
# hyprlock never stacks over the first.
|
||||
/*
|
||||
wayland.windowManager.hyprland.settings.bind = [
|
||||
"$mod, X, exec, ${pkgs.procps}/bin/pidof hyprlock || ${hyprlock}"
|
||||
"$mod CTRL, X, exec, ${idleLockToggle}"
|
||||
];
|
||||
*/
|
||||
registry.keybind.${config.networking.hostName} = {
|
||||
hyprlock-lock = {
|
||||
keys = "SUPER + X";
|
||||
action = {
|
||||
kind = "exec";
|
||||
# The guard drops the call when a locker is already up, so no manual
|
||||
# trigger stacks a second hyprlock over the first.
|
||||
command = "${lib.getExe' pkgs.procps "pidof"} hyprlock || ${lib.getExe pkgs.hyprlock}";
|
||||
};
|
||||
};
|
||||
|
||||
hyprlock-toggle-idle-inhibit = {
|
||||
keys = "SUPER + CTRL + X";
|
||||
action = {
|
||||
kind = "exec";
|
||||
command = "${idleLockToggle}";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
registry.waybar.${config.networking.hostName}."custom/idle-lock" = {
|
||||
order = 20;
|
||||
settings = {
|
||||
return-type = "json";
|
||||
exec = "${idleLockStatus}";
|
||||
interval = "once";
|
||||
signal = 9;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,9 +13,13 @@
|
||||
config = lib.mkIf config.modules.desktop.session.login.enable {
|
||||
services.greetd = {
|
||||
enable = true;
|
||||
settings.default_session.command =
|
||||
"${lib.getExe pkgs.tuigreet} --time --remember "
|
||||
+ "--cmd 'uwsm start -e -D Hyprland hyprland.desktop'";
|
||||
settings.default_session.command = lib.escapeShellArgs [
|
||||
(lib.getExe pkgs.tuigreet)
|
||||
"--time"
|
||||
"--remember"
|
||||
"--cmd"
|
||||
"${lib.getExe config.programs.uwsm.package} start -e -D Hyprland hyprland.desktop"
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,19 +2,29 @@
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
util,
|
||||
...
|
||||
}:
|
||||
# mako as the notification daemon: toasts, a do-not-disturb mode, and history recall.
|
||||
let
|
||||
user = config.user.name;
|
||||
dndStatus = pkgs.writeShellScript "waybar-dnd-status" ''
|
||||
if ${lib.getExe' pkgs.mako "makoctl"} mode 2>/dev/null | ${lib.getExe pkgs.gnugrep} -qx dnd; then
|
||||
printf '{"text":"${util.glyph "f1f6"}","tooltip":"Do not disturb: on","class":"dnd"}\n'
|
||||
else
|
||||
printf '{"text":"${util.glyph "f0f3"}","tooltip":"Notifications on","class":"active"}\n'
|
||||
fi
|
||||
'';
|
||||
|
||||
makoctl = "${pkgs.mako}/bin/makoctl";
|
||||
dndToggle = pkgs.writeShellScript "waybar-dnd-toggle" ''
|
||||
${lib.getExe' pkgs.mako "makoctl"} mode -t dnd >/dev/null 2>&1
|
||||
${lib.getExe' pkgs.procps "pkill"} -RTMIN+8 waybar
|
||||
'';
|
||||
in
|
||||
{
|
||||
options.modules.desktop.session.mako.enable = lib.mkEnableOption "the mako notification daemon";
|
||||
|
||||
config = lib.mkIf config.modules.desktop.session.mako.enable {
|
||||
home-manager.users.${user} = {
|
||||
home-manager.users.${config.user.name} = {
|
||||
# Colors and the popup font come from Stylix's mako target, so only
|
||||
# behaviour is set here.
|
||||
services.mako = {
|
||||
@@ -31,15 +41,25 @@ in
|
||||
"mode=dnd".invisible = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Recall the last notification from history, keyboard-only like the rest
|
||||
# of the session.
|
||||
# $mod is defined by the compositor config these binds share.
|
||||
/*
|
||||
wayland.windowManager.hyprland.settings.bind = [
|
||||
"$mod, N, exec, ${makoctl} restore"
|
||||
];
|
||||
*/
|
||||
registry.keybind.${config.networking.hostName}.mako-history = {
|
||||
keys = "SUPER + N";
|
||||
action = {
|
||||
kind = "exec";
|
||||
command = "${lib.getExe' pkgs.mako "makoctl"} restore";
|
||||
};
|
||||
};
|
||||
|
||||
registry.waybar.${config.networking.hostName}."custom/dnd" = {
|
||||
order = 80;
|
||||
settings = {
|
||||
return-type = "json";
|
||||
exec = "${dndStatus}";
|
||||
on-click = "${dndToggle}";
|
||||
interval = "once";
|
||||
signal = 8;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,19 +6,17 @@
|
||||
}:
|
||||
# rofi as the desktop launcher and dmenu-style menu frontend.
|
||||
let
|
||||
user = config.user.name;
|
||||
|
||||
# A session menu built on the same themed rofi, so the power actions look
|
||||
# like every other menu the launcher drives.
|
||||
powerMenu = pkgs.writeShellScript "rofi-power-menu" ''
|
||||
chosen=$(printf '%s\n' Lock Suspend 'Log out' Reboot 'Shut down' \
|
||||
| ${pkgs.rofi}/bin/rofi -dmenu -i -p Power)
|
||||
| ${lib.getExe pkgs.rofi} -dmenu -i -p Power)
|
||||
case "$chosen" in
|
||||
Lock) ${pkgs.systemd}/bin/loginctl lock-session ;;
|
||||
Suspend) ${pkgs.systemd}/bin/systemctl suspend ;;
|
||||
'Log out') ${pkgs.systemd}/bin/loginctl terminate-session "$XDG_SESSION_ID" ;;
|
||||
Reboot) ${pkgs.systemd}/bin/systemctl reboot ;;
|
||||
'Shut down') ${pkgs.systemd}/bin/systemctl poweroff ;;
|
||||
Lock) ${lib.getExe' pkgs.systemd "loginctl"} lock-session ;;
|
||||
Suspend) ${lib.getExe' pkgs.systemd "systemctl"} suspend ;;
|
||||
'Log out') ${lib.getExe' pkgs.systemd "loginctl"} terminate-session "$XDG_SESSION_ID" ;;
|
||||
Reboot) ${lib.getExe' pkgs.systemd "systemctl"} reboot ;;
|
||||
'Shut down') ${lib.getExe' pkgs.systemd "systemctl"} poweroff ;;
|
||||
esac
|
||||
'';
|
||||
in
|
||||
@@ -26,28 +24,43 @@ in
|
||||
options.modules.desktop.session.rofi.enable = lib.mkEnableOption "the rofi launcher";
|
||||
|
||||
config = lib.mkIf config.modules.desktop.session.rofi.enable {
|
||||
home-manager.users.${user} = {
|
||||
# No theme is set here: Stylix's rofi target supplies the Nord one.
|
||||
home-manager.users.${config.user.name} = hm: {
|
||||
# Stylix supplies the Nord theme.
|
||||
programs.rofi = {
|
||||
enable = true;
|
||||
|
||||
# Application launch is the only mode, kept minimal so the prompt opens
|
||||
# as fast as rofi can read the desktop entries.
|
||||
# Icons are off: resolving one per entry is the bulk of drun's startup.
|
||||
extraConfig = {
|
||||
settings = {
|
||||
font = lib.mkForce "JetBrains Mono 12";
|
||||
modi = "drun";
|
||||
show-icons = false;
|
||||
};
|
||||
|
||||
theme.window = {
|
||||
border = hm.config.lib.formats.rasi.mkLiteral "2px";
|
||||
border-color = hm.config.lib.formats.rasi.mkLiteral "#FFFFFF";
|
||||
border-radius = hm.config.lib.formats.rasi.mkLiteral "8px";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
registry.keybind.${config.networking.hostName} = {
|
||||
rofi-launcher = {
|
||||
keys = "SUPER + R";
|
||||
action = {
|
||||
kind = "exec";
|
||||
command = "${lib.getExe pkgs.rofi} -show drun";
|
||||
};
|
||||
};
|
||||
|
||||
# The launcher and the session menu, kept next to the tool they drive.
|
||||
# $mod is defined by the compositor config these binds share.
|
||||
/*
|
||||
wayland.windowManager.hyprland.settings.bind = [
|
||||
"$mod, R, exec, ${pkgs.rofi}/bin/rofi -show drun"
|
||||
"$mod SHIFT, X, exec, ${powerMenu}"
|
||||
];
|
||||
*/
|
||||
rofi-power-menu = {
|
||||
keys = "SUPER + SHIFT + X";
|
||||
action = {
|
||||
kind = "exec";
|
||||
command = "${powerMenu}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,97 +6,19 @@
|
||||
}:
|
||||
# Waybar: a top status bar reading system state at a glance.
|
||||
let
|
||||
vpnClient = config.modules.network.vpn-client;
|
||||
moduleNames =
|
||||
position:
|
||||
map (entry: entry.name) (
|
||||
lib.sort (left: right: left.module.order < right.module.order) (
|
||||
lib.filter (entry: entry.module.position == position) moduleEntries
|
||||
)
|
||||
);
|
||||
|
||||
# A Nerd Font Private-Use-Area codepoint as a literal character, decoded
|
||||
# through JSON so the glyph survives as bytes rather than an editor paste.
|
||||
g = code: builtins.fromJSON ''"\u${code}"'';
|
||||
user = config.user.name;
|
||||
moduleEntries = lib.mapAttrsToList (name: module: {
|
||||
inherit name module;
|
||||
}) modules;
|
||||
|
||||
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 }'
|
||||
'';
|
||||
modules = lib.attrByPath [ config.networking.hostName ] { } config.registry.waybar;
|
||||
in
|
||||
{
|
||||
options.modules.desktop.session.waybar.enable = lib.mkEnableOption "the Waybar status bar";
|
||||
@@ -106,64 +28,35 @@ in
|
||||
# codepoints Stylix's monospace font does not carry.
|
||||
fonts.packages = [ pkgs.nerd-fonts.symbols-only ];
|
||||
|
||||
home-manager.users.${user}.programs.waybar = {
|
||||
home-manager.users.${config.user.name}.programs.waybar = {
|
||||
enable = true;
|
||||
package = pkgs.alexion.waybar;
|
||||
|
||||
# A user service bound to graphical-session.target, which uwsm activates,
|
||||
# so the bar comes up with the session and no compositor exec-once.
|
||||
systemd.enable = true;
|
||||
|
||||
style = ''
|
||||
.modules-right {
|
||||
margin-right: 8px;
|
||||
settings.mainBar = lib.mkMerge [
|
||||
{
|
||||
layer = "top";
|
||||
position = "top";
|
||||
height = 30;
|
||||
spacing = 8;
|
||||
margin-right = 8;
|
||||
|
||||
modules-left = moduleNames "left";
|
||||
modules-center = moduleNames "center";
|
||||
modules-right = moduleNames "right";
|
||||
}
|
||||
(lib.mapAttrs (_name: module: module.settings) modules)
|
||||
];
|
||||
};
|
||||
|
||||
#custom-dnd {
|
||||
margin-right: 8px;
|
||||
}
|
||||
'';
|
||||
|
||||
settings.mainBar = {
|
||||
layer = "top";
|
||||
position = "top";
|
||||
height = 30;
|
||||
spacing = 8;
|
||||
|
||||
modules-left = [ "hyprland/workspaces" ];
|
||||
modules-center = [ "clock" ];
|
||||
modules-right = [
|
||||
"custom/recording"
|
||||
"custom/idle-lock"
|
||||
"mpris"
|
||||
"wireplumber"
|
||||
]
|
||||
++ 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<com.mitchellh.ghostty>" = g "f120";
|
||||
"class<.*[Ff]irefox.*>" = g "f269";
|
||||
"class<chromium.*>" = g "f268";
|
||||
"class<[Cc]ode>" = g "f121";
|
||||
"class<obsidian>" = g "f02d";
|
||||
"class<[Ss]ignal>" = g "f4ad";
|
||||
"class<[Ss]team>" = g "f1b6";
|
||||
"class<[Vv]esktop>" = g "f1ff";
|
||||
};
|
||||
};
|
||||
|
||||
clock = {
|
||||
registry.waybar.${config.networking.hostName} = {
|
||||
clock = {
|
||||
position = "center";
|
||||
order = 10;
|
||||
settings = {
|
||||
format = "{:%a %d %b %H:%M}";
|
||||
tooltip-format = "<tt>{calendar}</tt>";
|
||||
calendar = {
|
||||
@@ -172,77 +65,6 @@ in
|
||||
format.today = "<b><u>{}</u></b>";
|
||||
};
|
||||
};
|
||||
|
||||
mpris = {
|
||||
format = "${g "f001"} {title}";
|
||||
format-paused = "${g "f04c"} {title}";
|
||||
max-length = 40;
|
||||
on-click = "play-pause";
|
||||
on-scroll-up = "next";
|
||||
on-scroll-down = "previous";
|
||||
};
|
||||
|
||||
wireplumber = {
|
||||
format = "{icon} {volume}%";
|
||||
format-muted = "${g "f026"} muted";
|
||||
format-icons = [
|
||||
(g "f026")
|
||||
(g "f027")
|
||||
(g "f028")
|
||||
];
|
||||
on-click = "${pkgs.wireplumber}/bin/wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle";
|
||||
};
|
||||
|
||||
"custom/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,5 +1,30 @@
|
||||
{ config, lib, ... }:
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
util,
|
||||
...
|
||||
}:
|
||||
# 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";
|
||||
|
||||
@@ -12,5 +37,50 @@
|
||||
alsa.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,21 +6,15 @@
|
||||
}:
|
||||
# Clipboard history: cliphist records every copy, picked back through rofi.
|
||||
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 launcher drives.
|
||||
# decode is needed because list emits id-prefixed lines rather than the copied
|
||||
# bytes, so the chosen id has to be resolved back before it can be re-copied.
|
||||
picker = pkgs.writeShellScript "clipboard-picker" ''
|
||||
${cliphist} list \
|
||||
| ${rofi} -dmenu -i -p Clipboard \
|
||||
| ${cliphist} decode \
|
||||
| ${wl-copy}
|
||||
${lib.getExe pkgs.cliphist} list \
|
||||
| ${lib.getExe pkgs.rofi} -dmenu -i -p Clipboard \
|
||||
| ${lib.getExe pkgs.cliphist} decode \
|
||||
| ${lib.getExe' pkgs.wl-clipboard "wl-copy"}
|
||||
'';
|
||||
in
|
||||
{
|
||||
@@ -28,7 +22,7 @@ in
|
||||
lib.mkEnableOption "cliphist clipboard history";
|
||||
|
||||
config = lib.mkIf config.modules.desktop.utilities.clipboard.enable {
|
||||
home-manager.users.${user} = {
|
||||
home-manager.users.${config.user.name} = {
|
||||
# wl-copy and wl-paste on PATH, so the shell can pipe into and out of the
|
||||
# clipboard.
|
||||
# The watchers and picker above reach wl-clipboard by store path, so this
|
||||
@@ -38,13 +32,14 @@ in
|
||||
# Two watchers record text and images to history, bound to the graphical
|
||||
# session so uwsm starts and stops them with it.
|
||||
services.cliphist.enable = true;
|
||||
};
|
||||
|
||||
# $mod is defined by the compositor config these binds share.
|
||||
/*
|
||||
wayland.windowManager.hyprland.settings.bind = [
|
||||
"$mod SHIFT, V, exec, ${picker}"
|
||||
];
|
||||
*/
|
||||
registry.keybind.${config.networking.hostName}.clipboard-history = {
|
||||
keys = "SUPER + SHIFT + V";
|
||||
action = {
|
||||
kind = "exec";
|
||||
command = "${picker}";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,9 +5,6 @@
|
||||
...
|
||||
}:
|
||||
# SwayOSD: a transient on-screen popup for volume and brightness changes.
|
||||
let
|
||||
user = config.user.name;
|
||||
in
|
||||
{
|
||||
options.modules.desktop.utilities.osd.enable =
|
||||
lib.mkEnableOption "the SwayOSD volume and brightness popup";
|
||||
@@ -19,21 +16,41 @@ in
|
||||
# group write, so the server dims the panel without root.
|
||||
# Membership below grants the running session that access.
|
||||
services.udev.packages = [ pkgs.swayosd ];
|
||||
users.users.${user}.extraGroups = [ "video" ];
|
||||
users.users.${config.user.name}.extraGroups = [ "video" ];
|
||||
|
||||
# The server draws the popups, so it runs for the whole graphical session.
|
||||
# It binds to the target uwsm activates.
|
||||
home-manager.users.${user}.systemd.user.services.swayosd = {
|
||||
home-manager.users.${config.user.name}.systemd.user.services.swayosd = {
|
||||
Unit = {
|
||||
Description = "SwayOSD on-screen display server";
|
||||
PartOf = [ "graphical-session.target" ];
|
||||
After = [ "graphical-session.target" ];
|
||||
};
|
||||
Service = {
|
||||
ExecStart = "${pkgs.swayosd}/bin/swayosd-server";
|
||||
ExecStart = lib.getExe' pkgs.swayosd "swayosd-server";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
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,41 +2,44 @@
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
util,
|
||||
...
|
||||
}:
|
||||
# Keybound screen recording: pick a region, then toggle a video-only capture.
|
||||
let
|
||||
user = config.user.name;
|
||||
# A single key starts and stops recording.
|
||||
# Waybar refreshes on state changes instead of polling.
|
||||
recordingToggle = pkgs.writeShellScript "screen-record-toggle" ''
|
||||
refresh_waybar() {
|
||||
${lib.getExe' pkgs.procps "pkill"} -RTMIN+10 waybar >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
slurp = "${pkgs.slurp}/bin/slurp";
|
||||
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
|
||||
if ${lib.getExe' pkgs.procps "pgrep"} -x wf-recorder >/dev/null; then
|
||||
${lib.getExe' pkgs.procps "pkill"} -INT -x wf-recorder
|
||||
exit 0
|
||||
fi
|
||||
|
||||
region=$(${slurp}) || exit 0
|
||||
dir="$(${xdgUserDir} VIDEOS)/Recordings"
|
||||
${mkdir} -p "$dir"
|
||||
file="$dir/recording-$(${date} +%Y%m%d-%H%M%S).mp4"
|
||||
region=$(${lib.getExe pkgs.slurp}) || exit 0
|
||||
dir="$(${lib.getExe' pkgs.xdg-user-dirs "xdg-user-dir"} VIDEOS)/Recordings"
|
||||
${lib.getExe' pkgs.coreutils "mkdir"} -p "$dir"
|
||||
file="$dir/recording-$(${lib.getExe' pkgs.coreutils "date"} +%Y%m%d-%H%M%S).mp4"
|
||||
|
||||
${notify-send} -a "Screen recording" "Recording started" "Region capture, no audio."
|
||||
${wf-recorder} -g "$region" -f "$file"
|
||||
${notify-send} -a "Screen recording" "Recording saved" "$file"
|
||||
${lib.getExe' pkgs.libnotify "notify-send"} -a "Screen recording" "Recording started" "Region capture, no audio."
|
||||
${lib.getExe pkgs.wf-recorder} -g "$region" -f "$file" &
|
||||
recorder=$!
|
||||
refresh_waybar
|
||||
wait "$recorder" || true
|
||||
refresh_waybar
|
||||
${lib.getExe' pkgs.libnotify "notify-send"} -a "Screen recording" "Recording saved" "$file"
|
||||
'';
|
||||
|
||||
# Empty text collapses the widget when recording is idle.
|
||||
recordingStatus = pkgs.writeShellScript "waybar-recording-status" ''
|
||||
if ${lib.getExe' pkgs.procps "pgrep"} -x wf-recorder >/dev/null; then
|
||||
printf '{"text":"${util.glyph "f03d"}","tooltip":"Recording","class":"recording"}\n'
|
||||
else
|
||||
printf '{"text":"","class":"idle"}\n'
|
||||
fi
|
||||
'';
|
||||
in
|
||||
{
|
||||
@@ -44,13 +47,22 @@ in
|
||||
lib.mkEnableOption "wf-recorder screen recording";
|
||||
|
||||
config = lib.mkIf config.modules.desktop.utilities.recording.enable {
|
||||
home-manager.users.${user} = {
|
||||
# $mod is defined by the compositor config these binds share.
|
||||
/*
|
||||
wayland.windowManager.hyprland.settings.bind = [
|
||||
"$mod SHIFT, R, exec, ${toggle}"
|
||||
];
|
||||
*/
|
||||
registry.keybind.${config.networking.hostName}.recording-toggle = {
|
||||
keys = "SUPER + SHIFT + R";
|
||||
action = {
|
||||
kind = "exec";
|
||||
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
|
||||
# annotation. Satty copies annotated screenshots when it saves them.
|
||||
let
|
||||
user = config.user.name;
|
||||
|
||||
grimblast = "${pkgs.grimblast}/bin/grimblast";
|
||||
satty = "${pkgs.satty}/bin/satty";
|
||||
tee = "${pkgs.coreutils}/bin/tee";
|
||||
wl-copy = "${pkgs.wl-clipboard}/bin/wl-copy";
|
||||
xdgUserDir = "${pkgs.xdg-user-dirs}/bin/xdg-user-dir";
|
||||
screenshotKey = keys: target: {
|
||||
inherit keys;
|
||||
action = {
|
||||
kind = "exec";
|
||||
command = "${capture target}";
|
||||
};
|
||||
};
|
||||
|
||||
# Copy the original screenshot before Satty opens.
|
||||
# Satty copies and saves annotated screenshots when the user presses Enter.
|
||||
capture =
|
||||
target:
|
||||
pkgs.writeShellScript "screenshot-${target}" ''
|
||||
dir="$(${xdgUserDir} PICTURES)/Screenshots"
|
||||
mkdir -p "$dir"
|
||||
${grimblast} save ${target} - \
|
||||
| ${tee} >(${wl-copy}) \
|
||||
| ${satty} --filename - \
|
||||
dir="$(${lib.getExe' pkgs.xdg-user-dirs "xdg-user-dir"} PICTURES)/Screenshots"
|
||||
${lib.getExe' pkgs.coreutils "mkdir"} -p "$dir"
|
||||
${lib.getExe pkgs.grimblast} save ${target} - \
|
||||
| ${lib.getExe' pkgs.coreutils "tee"} >(${lib.getExe' pkgs.wl-clipboard "wl-copy"}) \
|
||||
| ${lib.getExe pkgs.satty} --filename - \
|
||||
--output-filename "$dir/screenshot-%Y%m%d-%H%M%S.png" \
|
||||
--copy-command ${wl-copy} \
|
||||
--copy-command ${lib.getExe' pkgs.wl-clipboard "wl-copy"} \
|
||||
--save-after-copy \
|
||||
--early-exit \
|
||||
--actions-on-enter save-to-clipboard
|
||||
@@ -37,27 +37,21 @@ in
|
||||
lib.mkEnableOption "screenshots via grimblast and satty";
|
||||
|
||||
config = lib.mkIf config.modules.desktop.utilities.screenshot.enable {
|
||||
home-manager.users.${user} = {
|
||||
home-manager.users.${config.user.name} = {
|
||||
# satty is a one-shot annotation surface, so float it rather than letting
|
||||
# it claim a tile in the layout.
|
||||
# windowrule v3 syntax: space-separated field/value tokens, comma
|
||||
# separated, with matchers under a match: prefix and effects bare.
|
||||
/*
|
||||
wayland.windowManager.hyprland.settings.windowrule = [
|
||||
"float 1, match:class ^(com\\.gabm\\.satty)$"
|
||||
wayland.windowManager.hyprland.settings.window_rule = [
|
||||
{
|
||||
match.class = "^(com\\.gabm\\.satty)$";
|
||||
float = true;
|
||||
}
|
||||
];
|
||||
*/
|
||||
};
|
||||
|
||||
# Print with plain/Shift/Ctrl for region/window/full.
|
||||
# Super+L is already the hjkl focus and movement bind, so screenshots take
|
||||
# the Print key instead.
|
||||
/*
|
||||
wayland.windowManager.hyprland.settings.bind = [
|
||||
", Print, exec, ${capture "area"}"
|
||||
"SHIFT, Print, exec, ${capture "active"}"
|
||||
"CTRL, Print, exec, ${capture "screen"}"
|
||||
];
|
||||
*/
|
||||
registry.keybind.${config.networking.hostName} = {
|
||||
screenshot-area = screenshotKey "Print" "area";
|
||||
screenshot-active = screenshotKey "SHIFT + Print" "active";
|
||||
screenshot-screen = screenshotKey "CTRL + Print" "screen";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,15 +6,13 @@
|
||||
}:
|
||||
# Nord theming for the graphical layer, driven from one base16 scheme by Stylix.
|
||||
let
|
||||
|
||||
# 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.
|
||||
wallpaper = pkgs.runCommand "nord-wallpaper.png" { } ''
|
||||
${pkgs.imagemagick}/bin/magick \
|
||||
${lib.getExe pkgs.imagemagick} \
|
||||
-size 2560x1440 gradient:'#2E3440'-'#3B4252' \
|
||||
"$out"
|
||||
'';
|
||||
user = config.user.name;
|
||||
in
|
||||
{
|
||||
options.modules.desktop.utilities.theming.enable =
|
||||
@@ -59,7 +57,7 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
home-manager.users.${user} = {
|
||||
home-manager.users.${config.user.name} = {
|
||||
# Stylix drives the cursor through home-manager's pointer-cursor config,
|
||||
# whose generation must be switched on explicitly.
|
||||
home.pointerCursor.enable = true;
|
||||
@@ -68,5 +66,10 @@ in
|
||||
# base16 mapping Stylix would apply, so its target stays off.
|
||||
stylix.targets.nixvim.enable = false;
|
||||
};
|
||||
|
||||
registry.env.${config.networking.hostName} = {
|
||||
HYPRCURSOR_THEME = config.stylix.cursor.name;
|
||||
HYPRCURSOR_SIZE = toString config.stylix.cursor.size;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
{ config, lib, ... }:
|
||||
# Declares the XDG user directories, so a tool that queries them lands in a
|
||||
# well-known folder a host can relocate from one place.
|
||||
# Declares XDG user directories, so tools that query them use predictable
|
||||
# folders a host can relocate from one place.
|
||||
let
|
||||
user = config.user.name;
|
||||
homeDirectory = "/home/${config.user.name}";
|
||||
in
|
||||
{
|
||||
options.modules.desktop.utilities.userdirs.enable = lib.mkEnableOption "XDG user directories";
|
||||
|
||||
config = lib.mkIf config.modules.desktop.utilities.userdirs.enable {
|
||||
# Write ~/.config/user-dirs.dirs from the option defaults.
|
||||
home-manager.users.${user}.xdg.userDirs.enable = true;
|
||||
home-manager.users.${config.user.name}.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,7 +30,6 @@ in
|
||||
modules.network.ssh.userKey.sopsFile = hostSecrets;
|
||||
|
||||
services.xserver.xkb.layout = "us";
|
||||
services.xserver.xkb.options = "caps:escape_shifted_capslock";
|
||||
console.useXkbConfig = true;
|
||||
|
||||
sops.defaultSopsFile = ../../secrets/shared.yaml;
|
||||
@@ -54,6 +53,7 @@ in
|
||||
home.username = config.user.name;
|
||||
home.homeDirectory = "/home/${config.user.name}";
|
||||
home.stateVersion = config.system.stateVersion;
|
||||
home.sessionVariables = lib.attrByPath [ config.networking.hostName ] { } config.registry.env;
|
||||
home.packages = with pkgs; [
|
||||
eza
|
||||
unzip
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
}:
|
||||
# Imports existing ZFS pools that hold durable service state.
|
||||
let
|
||||
cfg = config.modules.host.zfs; # NOLINT(let_order)
|
||||
cfg = config.modules.host.zfs;
|
||||
in
|
||||
{
|
||||
options.modules.host.zfs = {
|
||||
|
||||
57
configs/nixos/modules/network/default.nix
Normal file
57
configs/nixos/modules/network/default.nix
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
util,
|
||||
...
|
||||
}:
|
||||
# NetworkManager-managed networking.
|
||||
let
|
||||
networkStatus = pkgs.writeShellScript "waybar-network-status" ''
|
||||
active=$(
|
||||
${lib.getExe' pkgs.networkmanager "nmcli"} -t -f DEVICE,TYPE,STATE,CONNECTION device status \
|
||||
| ${lib.getExe' pkgs.gawk "awk"} -F: '$3 == "connected" && $2 != "loopback" { print; exit }'
|
||||
)
|
||||
|
||||
if [ -z "$active" ]; then
|
||||
${lib.getExe pkgs.jq} -cn \
|
||||
--arg text "${util.glyph "f127"} off" \
|
||||
--arg tooltip "Network disconnected" \
|
||||
--arg class "disconnected" \
|
||||
'{ text: $text, tooltip: $tooltip, class: $class }'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
IFS=: read -r device type _state connection <<< "$active"
|
||||
|
||||
case "$type" in
|
||||
wifi) icon="${util.glyph "f1eb"}" ;;
|
||||
ethernet) icon="LAN" ;;
|
||||
*) icon="NET" ;;
|
||||
esac
|
||||
|
||||
ipaddr=$(${lib.getExe' pkgs.iproute2 "ip"} -4 -o addr show dev "$device" | ${lib.getExe' pkgs.gawk "awk"} '{ print $4; exit }')
|
||||
|
||||
${lib.getExe pkgs.jq} -cn \
|
||||
--arg text "$icon $connection" \
|
||||
--arg tooltip "$device: ''${ipaddr:-no IPv4}" \
|
||||
--arg class "$type" \
|
||||
'{ text: $text, tooltip: $tooltip, class: $class }'
|
||||
'';
|
||||
in
|
||||
{
|
||||
options.modules.network.enable = lib.mkEnableOption "NetworkManager-managed networking";
|
||||
|
||||
config = lib.mkIf config.modules.network.enable {
|
||||
networking.networkmanager.enable = true;
|
||||
|
||||
registry.waybar.${config.networking.hostName}."custom/network" = {
|
||||
order = 60;
|
||||
settings = {
|
||||
return-type = "json";
|
||||
exec = "${networkStatus}";
|
||||
interval = 5;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -7,19 +7,17 @@
|
||||
# Configures inbound and outbound SSH access.
|
||||
let
|
||||
cfg = config.modules.network.ssh;
|
||||
user = config.user.name; # NOLINT(let_order)
|
||||
user = config.user.name;
|
||||
|
||||
# NOLINT(let_order)
|
||||
workstationNames = [
|
||||
"neogaia"
|
||||
"zeus"
|
||||
];
|
||||
serverNames = [ "pikachu" ];
|
||||
|
||||
isWorkstation = lib.elem config.networking.hostName workstationNames; # NOLINT(let_order)
|
||||
isServer = lib.elem config.networking.hostName serverNames; # NOLINT(let_order)
|
||||
isWorkstation = lib.elem config.networking.hostName workstationNames;
|
||||
isServer = lib.elem config.networking.hostName serverNames;
|
||||
|
||||
# NOLINT(let_order)
|
||||
clientKeyFor = host: builtins.readFile (../../hosts + "/${host}/ssh_user_ed25519_key.pub");
|
||||
hostKeysFor =
|
||||
host:
|
||||
@@ -28,7 +26,6 @@ let
|
||||
(../../hosts + "/${host}/ssh_host_rsa_key.pub")
|
||||
];
|
||||
|
||||
# NOLINT(let_order)
|
||||
managedTargets = lib.genAttrs (workstationNames ++ serverNames) (name: {
|
||||
hostName = registry.address.${name}.self;
|
||||
clientKey = clientKeyFor name;
|
||||
@@ -48,7 +45,6 @@ let
|
||||
};
|
||||
};
|
||||
|
||||
# NOLINT(let_order)
|
||||
mkTarget = name: target: {
|
||||
hostName = target.hostName or name;
|
||||
user = target.user or user;
|
||||
@@ -57,15 +53,14 @@ let
|
||||
clientKey = target.clientKey or null;
|
||||
hostKeys = target.hostKeys or [ ];
|
||||
};
|
||||
targets = lib.mapAttrs mkTarget (managedTargets // extraTargets); # NOLINT(let_order)
|
||||
targets = lib.mapAttrs mkTarget (managedTargets // extraTargets);
|
||||
|
||||
clientKeysFor = names: lib.filter (key: key != null) (map (name: targets.${name}.clientKey) names); # NOLINT(let_order)
|
||||
# NOLINT(let_order)
|
||||
clientKeysFor = names: lib.filter (key: key != null) (map (name: targets.${name}.clientKey) names);
|
||||
defaultAuthorizedKeys = lib.flatten (
|
||||
clientKeysFor workstationNames ++ lib.optional isServer (clientKeysFor serverNames)
|
||||
);
|
||||
|
||||
outboundTargetNames = # NOLINT(let_order)
|
||||
outboundTargetNames =
|
||||
let
|
||||
groupTargets =
|
||||
if isServer then
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
}:
|
||||
# Configures bridges for tagged VLANs on a physical interface that uses DHCP.
|
||||
let
|
||||
cfg = config.modules.network.vlans; # NOLINT(let_order)
|
||||
cfg = config.modules.network.vlans;
|
||||
in
|
||||
{
|
||||
options.modules.network.vlans = {
|
||||
|
||||
@@ -1,10 +1,34 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
util,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.modules.network.vpn-client; # NOLINT(let_order)
|
||||
cfg = config.modules.network.vpn-client;
|
||||
|
||||
vpnIcon = util.glyph [
|
||||
"db83"
|
||||
"dc9d"
|
||||
];
|
||||
|
||||
vpnStatus = pkgs.writeShellScript "waybar-vpn-status" ''
|
||||
interface=${lib.escapeShellArg cfg.interface}
|
||||
|
||||
if ! ${lib.getExe' pkgs.iproute2 "ip"} link show dev "$interface" >/dev/null 2>&1; then
|
||||
printf '{"text":"","class":"idle"}\n'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ipaddr=$(${lib.getExe' pkgs.iproute2 "ip"} -4 -o addr show dev "$interface" | ${lib.getExe' pkgs.gawk "awk"} '{ print $4; exit }')
|
||||
|
||||
${lib.getExe pkgs.jq} -cn \
|
||||
--arg text "${vpnIcon}" \
|
||||
--arg tooltip "VPN $interface: ''${ipaddr:-no IPv4}" \
|
||||
--arg class "active" \
|
||||
'{ text: $text, tooltip: $tooltip, class: $class }'
|
||||
'';
|
||||
in
|
||||
{
|
||||
options.modules.network.vpn-client = {
|
||||
@@ -57,6 +81,15 @@ in
|
||||
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} = {
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.modules.services.actualbudget; # NOLINT(let_order)
|
||||
in
|
||||
{
|
||||
options.modules.services.actualbudget = {
|
||||
enable = lib.mkEnableOption "Actual Budget";
|
||||
|
||||
port = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 5006;
|
||||
description = "Port Actual Budget listens on.";
|
||||
};
|
||||
|
||||
dataDir = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "/data";
|
||||
description = "Directory where Actual Budget stores server and user files.";
|
||||
};
|
||||
|
||||
user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "actual";
|
||||
description = "User account that runs Actual Budget.";
|
||||
};
|
||||
|
||||
group = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "storage";
|
||||
description = "Group account that owns Actual Budget's writable state.";
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
config = lib.mkMerge [
|
||||
{
|
||||
registry."stable-uid".${config.networking.hostName}.actualbudget = 20001;
|
||||
}
|
||||
|
||||
(lib.mkIf cfg.enable {
|
||||
users.users.${cfg.user} = {
|
||||
isSystemUser = true;
|
||||
uid = config.registry."stable-uid".${config.networking.hostName}.actualbudget;
|
||||
inherit (cfg) group;
|
||||
};
|
||||
|
||||
services.actual = {
|
||||
enable = true;
|
||||
openFirewall = true;
|
||||
inherit (cfg) user;
|
||||
inherit (cfg) group;
|
||||
settings = {
|
||||
inherit (cfg) port dataDir;
|
||||
};
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -2,147 +2,135 @@
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
util,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.modules.services.copyparty; # NOLINT(let_order)
|
||||
cfg = config.modules.services.copyparty;
|
||||
|
||||
volumeType = lib.types.submodule {
|
||||
options = {
|
||||
path = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Filesystem path served by this volume.";
|
||||
};
|
||||
accountName = config.user.name;
|
||||
|
||||
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.";
|
||||
};
|
||||
serviceGroup = "storage";
|
||||
|
||||
readOnly = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = "Serve this volume read-only.";
|
||||
};
|
||||
serviceStateDir = "/var/lib/copyparty";
|
||||
|
||||
flags = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "dthumb" ];
|
||||
description = "Copyparty volume flags.";
|
||||
};
|
||||
};
|
||||
serviceUser = "copyparty";
|
||||
|
||||
writableVolumes = lib.filter (volume: !volume.readOnly) (lib.attrValues cfg.volumes);
|
||||
|
||||
port = 3923;
|
||||
defaultAccess = {
|
||||
A = [ accountName ];
|
||||
};
|
||||
|
||||
configScript = pkgs.writeShellScript "copyparty-config" ''
|
||||
set -eu
|
||||
defaultWritableFlags = {
|
||||
d2d = true;
|
||||
d2t = true;
|
||||
dthumb = true;
|
||||
};
|
||||
|
||||
umask 077
|
||||
password="$(${pkgs.coreutils}/bin/tr -d '\n' < "$CREDENTIALS_DIRECTORY/password")"
|
||||
credentialHash = builtins.substring 0 16 (builtins.hashString "sha256" accountName);
|
||||
|
||||
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
|
||||
credentialName = "password-${credentialHash}";
|
||||
|
||||
${renderedVolumes}
|
||||
EOF
|
||||
'';
|
||||
renderAccess = permission: accounts: " ${permission}: ${lib.concatStringsSep ", " accounts}";
|
||||
passwordPlaceholder = "@COPYPARTY_PASSWORD_${credentialHash}@";
|
||||
|
||||
hasFlag =
|
||||
name: flags: lib.any (flag: lib.hasPrefix "${name}:" flag || lib.hasPrefix "${name}=" flag) flags;
|
||||
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";
|
||||
};
|
||||
|
||||
defaultPermissionFlags =
|
||||
globalExtraConfig = lib.optionalString cfg.reverseProxy.enable (
|
||||
lib.concatMapStringsSep "\n" (source: "xff-src: ${source}") cfg.reverseProxy.trustedSources
|
||||
);
|
||||
|
||||
volumeFlags =
|
||||
flags:
|
||||
lib.optionals (!(hasFlag "chmod_d" flags)) [ "chmod_d: 2770" ]
|
||||
++ lib.optionals (!(hasFlag "chmod_f" flags)) [ "chmod_f: 660" ];
|
||||
lib.optionalAttrs (flags != { }) (
|
||||
{
|
||||
chmod_d = "2770";
|
||||
chmod_f = "660";
|
||||
}
|
||||
// flags
|
||||
);
|
||||
|
||||
renderFlags =
|
||||
flags: lib.concatMapStringsSep "\n" (flag: " ${flag}") (flags ++ defaultPermissionFlags 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;
|
||||
|
||||
renderVolume = urlPath: volume: ''
|
||||
[${urlPath}]
|
||||
${volume.path}
|
||||
accs:
|
||||
${lib.concatStringsSep "\n" (lib.mapAttrsToList renderAccess volume.access)}
|
||||
${lib.optionalString (volume.flags != [ ]) ''
|
||||
flags:
|
||||
${renderFlags volume.flags}
|
||||
''}
|
||||
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
|
||||
'';
|
||||
|
||||
renderTrustedSource = source: " xff-src: ${source}";
|
||||
|
||||
renderedReverseProxy = lib.optionalString cfg.reverseProxy.enable ''
|
||||
rproxy: 1
|
||||
xff-hdr: x-forwarded-for
|
||||
${lib.concatStringsSep "\n" (map renderTrustedSource cfg.reverseProxy.trustedSources)}
|
||||
'';
|
||||
|
||||
renderedVolumes = lib.concatStringsSep "\n" (lib.mapAttrsToList renderVolume cfg.volumes);
|
||||
|
||||
in
|
||||
{
|
||||
options.modules.services.copyparty = {
|
||||
enable = lib.mkEnableOption "Copyparty";
|
||||
|
||||
package = lib.mkPackageOption pkgs "copyparty-min" { };
|
||||
|
||||
port = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 3923;
|
||||
description = "Port Copyparty listens on.";
|
||||
};
|
||||
|
||||
user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "copyparty";
|
||||
description = "User account that runs Copyparty.";
|
||||
};
|
||||
|
||||
group = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "storage";
|
||||
description = "Group account that owns Copyparty's writable state and served files.";
|
||||
};
|
||||
|
||||
accountName = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "alexion";
|
||||
description = "Copyparty account name configured for password authentication.";
|
||||
};
|
||||
|
||||
passwordFile = lib.mkOption {
|
||||
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.";
|
||||
description = "Sets the runtime file that contains the Copyparty account password.";
|
||||
};
|
||||
|
||||
volumes = lib.mkOption {
|
||||
type = lib.types.attrsOf volumeType;
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule {
|
||||
options = {
|
||||
path = lib.mkOption {
|
||||
type = lib.types.externalPath;
|
||||
description = "Sets the filesystem path served by this volume.";
|
||||
};
|
||||
|
||||
access = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
|
||||
default = { };
|
||||
example = {
|
||||
A = [ "alexion" ];
|
||||
};
|
||||
description = "Maps Copyparty permission strings to account names.";
|
||||
};
|
||||
|
||||
readOnly = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = "Serves this volume read-only.";
|
||||
};
|
||||
|
||||
flags = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
example = {
|
||||
dthumb = true;
|
||||
};
|
||||
description = "Sets Copyparty volume flags.";
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
default = { };
|
||||
description = "Copyparty volumes keyed by their URL path.";
|
||||
description = "Defines Copyparty volumes by URL path.";
|
||||
};
|
||||
|
||||
reverseProxy = {
|
||||
@@ -151,7 +139,7 @@ in
|
||||
trustedSources = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
description = "IP ranges that may supply Copyparty's forwarded client IP header.";
|
||||
description = "Lists proxy sources that can set Copyparty's forwarded client IP header.";
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -173,46 +161,49 @@ in
|
||||
}
|
||||
];
|
||||
|
||||
users.users.${cfg.user} = {
|
||||
users.users.${serviceUser} = {
|
||||
isSystemUser = true;
|
||||
uid = config.registry."stable-uid".${config.networking.hostName}.copyparty;
|
||||
inherit (cfg) group;
|
||||
home = cfg.stateDir;
|
||||
group = serviceGroup;
|
||||
home = serviceStateDir;
|
||||
};
|
||||
|
||||
systemd.services.copyparty = {
|
||||
description = "Copyparty file server";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
preStart = ''
|
||||
${lib.getExe' pkgs.coreutils "install"} -m 600 ${configFile} /run/copyparty/copyparty.conf
|
||||
${replacePassword}
|
||||
'';
|
||||
serviceConfig = {
|
||||
ReloadSignal = "SIGUSR1";
|
||||
Type = "simple";
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
WorkingDirectory = cfg.stateDir;
|
||||
User = serviceUser;
|
||||
Group = serviceGroup;
|
||||
WorkingDirectory = serviceStateDir;
|
||||
Environment = [
|
||||
"HOME=${cfg.stateDir}"
|
||||
"XDG_CONFIG_HOME=${cfg.stateDir}"
|
||||
"HOME=${serviceStateDir}"
|
||||
"XDG_CONFIG_HOME=${serviceStateDir}"
|
||||
];
|
||||
StateDirectory = "copyparty";
|
||||
RuntimeDirectory = "copyparty";
|
||||
RuntimeDirectoryMode = "0700";
|
||||
LoadCredential = [ "password:${cfg.passwordFile}" ];
|
||||
ExecStartPre = configScript;
|
||||
ExecStart = "${lib.getExe cfg.package} -c /run/copyparty/copyparty.conf";
|
||||
LoadCredential = [ "${credentialName}:${cfg.passwordFile}" ];
|
||||
ExecStart = "${lib.getExe pkgs.copyparty-min} -c /run/copyparty/copyparty.conf";
|
||||
Restart = "on-failure";
|
||||
NoNewPrivileges = true;
|
||||
PrivateTmp = true;
|
||||
ProtectSystem = "strict";
|
||||
ProtectHome = true;
|
||||
ReadWritePaths = [
|
||||
cfg.stateDir
|
||||
serviceStateDir
|
||||
]
|
||||
++ map (volume: volume.path) (lib.filter (volume: !volume.readOnly) (lib.attrValues cfg.volumes));
|
||||
++ map (volume: volume.path) writableVolumes;
|
||||
};
|
||||
};
|
||||
|
||||
networking.firewall.allowedTCPPorts = [ cfg.port ];
|
||||
networking.firewall.allowedTCPPorts = [ port ];
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
@@ -92,33 +92,33 @@
|
||||
startScript = pkgs.writeShellScript "palworld-start" ''
|
||||
set -eu
|
||||
|
||||
${pkgs.coreutils}/bin/install -d \
|
||||
${lib.getExe' pkgs.coreutils "install"} -d \
|
||||
-m 0770 \
|
||||
${lib.escapeShellArg service.stateDir} \
|
||||
${lib.escapeShellArg (builtins.dirOf savedDir)}
|
||||
|
||||
if [ -L ${lib.escapeShellArg savedDir} ]; then
|
||||
current_target="$(${pkgs.coreutils}/bin/readlink ${lib.escapeShellArg savedDir})"
|
||||
current_target="$(${lib.getExe' pkgs.coreutils "readlink"} ${lib.escapeShellArg savedDir})"
|
||||
if [ "$current_target" != ${lib.escapeShellArg service.stateDir} ]; then
|
||||
${pkgs.coreutils}/bin/rm ${lib.escapeShellArg savedDir}
|
||||
${lib.getExe' pkgs.coreutils "rm"} ${lib.escapeShellArg savedDir}
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -d ${lib.escapeShellArg savedDir} ] && [ ! -L ${lib.escapeShellArg savedDir} ]; then
|
||||
if ${pkgs.findutils}/bin/find ${lib.escapeShellArg service.stateDir} -mindepth 1 -maxdepth 1 | ${pkgs.gnugrep}/bin/grep -q .; then
|
||||
${pkgs.coreutils}/bin/rm -rf ${lib.escapeShellArg savedDir}
|
||||
if ${lib.getExe' pkgs.findutils "find"} ${lib.escapeShellArg service.stateDir} -mindepth 1 -maxdepth 1 | ${lib.getExe' pkgs.gnugrep "grep"} -q .; then
|
||||
${lib.getExe' pkgs.coreutils "rm"} -rf ${lib.escapeShellArg savedDir}
|
||||
else
|
||||
${pkgs.coreutils}/bin/cp -a ${lib.escapeShellArg savedDir}/. ${lib.escapeShellArg service.stateDir}/
|
||||
${pkgs.coreutils}/bin/rm -rf ${lib.escapeShellArg savedDir}
|
||||
${lib.getExe' pkgs.coreutils "cp"} -a ${lib.escapeShellArg savedDir}/. ${lib.escapeShellArg service.stateDir}/
|
||||
${lib.getExe' pkgs.coreutils "rm"} -rf ${lib.escapeShellArg savedDir}
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -e ${lib.escapeShellArg savedDir} ]; then
|
||||
${pkgs.coreutils}/bin/ln -s ${lib.escapeShellArg service.stateDir} ${lib.escapeShellArg savedDir}
|
||||
${lib.getExe' pkgs.coreutils "ln"} -s ${lib.escapeShellArg service.stateDir} ${lib.escapeShellArg savedDir}
|
||||
fi
|
||||
|
||||
password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.passwordFile})"
|
||||
admin_password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.adminPasswordFile})"
|
||||
password="$(${lib.getExe' pkgs.coreutils "tr"} -d '\n' < ${lib.escapeShellArg service.passwordFile})"
|
||||
admin_password="$(${lib.getExe' pkgs.coreutils "tr"} -d '\n' < ${lib.escapeShellArg service.adminPasswordFile})"
|
||||
|
||||
exec ./PalServer.sh \
|
||||
"$@" \
|
||||
@@ -145,9 +145,12 @@
|
||||
]
|
||||
++ lib.optional service.publicLobby "-publiclobby"
|
||||
++ lib.optionals (service.publicIP != null) [ "-publicip=${service.publicIP}" ]
|
||||
++ lib.optional service.rcon.enable "-rcon";
|
||||
++ lib.optionals service.rcon.enable [
|
||||
"-rcon"
|
||||
"-RCONPort=${toString service.rcon.port}"
|
||||
];
|
||||
udpPorts = [ service.port ];
|
||||
tcpPorts = [ service.port ];
|
||||
tcpPorts = lib.optional service.rcon.enable service.rcon.port;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
startScript = pkgs.writeShellScript "valheim-start" ''
|
||||
set -eu
|
||||
|
||||
password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.passwordFile})"
|
||||
password="$(${lib.getExe' pkgs.coreutils "tr"} -d '\n' < ${lib.escapeShellArg service.passwordFile})"
|
||||
exec ./valheim_server.x86_64 "$@" -password "$password"
|
||||
'';
|
||||
in
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.modules.services.homepage; # NOLINT(let_order)
|
||||
cfg = config.modules.services.homepage;
|
||||
|
||||
derivedAllowedHosts = lib.unique (
|
||||
lib.optional (selfRoute != null) selfRoute.domain
|
||||
@@ -111,7 +111,8 @@ let
|
||||
let
|
||||
sectionOrder = sectionOrderByTab.${tab} or [ ];
|
||||
in
|
||||
sectionOrder ++ lib.filter (section: !lib.elem section sectionOrder) (lib.attrNames sections);
|
||||
lib.filter (section: lib.hasAttr section sections) sectionOrder
|
||||
++ lib.filter (section: !lib.elem section sectionOrder) (lib.attrNames sections);
|
||||
|
||||
in
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.modules.services.navidrome; # NOLINT(let_order)
|
||||
cfg = config.modules.services.navidrome;
|
||||
in
|
||||
{
|
||||
options.modules.services.navidrome = {
|
||||
|
||||
@@ -40,43 +40,14 @@ let
|
||||
proxy_pass ${stream.address}:${toString stream.port};
|
||||
}
|
||||
'') generatedUdpStreams;
|
||||
generatedVirtualHosts =
|
||||
if duplicateDomainMessages != [ ] then
|
||||
throw (builtins.head duplicateDomainMessages)
|
||||
else
|
||||
lib.listToAttrs generatedVirtualHostList;
|
||||
generatedVirtualHosts = lib.listToAttrs generatedVirtualHostList;
|
||||
|
||||
duplicateDomainMessages = lib.concatLists (
|
||||
lib.mapAttrsToList (
|
||||
domain: virtualHosts:
|
||||
if lib.length virtualHosts > 1 then
|
||||
[
|
||||
"duplicate route domain ${domain} in ${
|
||||
lib.concatMapStringsSep " and " (
|
||||
virtualHost: mkRegistryPath virtualHost.registryName virtualHost.host virtualHost.entry
|
||||
) virtualHosts
|
||||
}"
|
||||
]
|
||||
else
|
||||
[ ]
|
||||
) virtualHostsByDomain
|
||||
);
|
||||
|
||||
virtualHostsByDomain = lib.foldl' (
|
||||
acc: virtualHost:
|
||||
acc
|
||||
// {
|
||||
${virtualHost.name} = (acc.${virtualHost.name} or [ ]) ++ [ virtualHost ];
|
||||
}
|
||||
) { } generatedVirtualHostList;
|
||||
|
||||
generatedVirtualHostList = generatedHttpsVirtualHostList ++ generatedHttpVirtualHostList;
|
||||
|
||||
generatedHttpsVirtualHostList = lib.concatLists (
|
||||
# Route registry validation already requires unique domains.
|
||||
generatedVirtualHostList = lib.concatLists (
|
||||
lib.mapAttrsToList (
|
||||
host: routes:
|
||||
lib.mapAttrsToList (entry: _route: mkVirtualHost host entry "https") (
|
||||
lib.filterAttrs (_entry: route: route.protocol == "https") routes
|
||||
lib.mapAttrsToList (entry: route: mkVirtualHost host entry route.protocol) (
|
||||
lib.filterAttrs (_entry: route: route.protocol != "udp") routes
|
||||
)
|
||||
) registry.route
|
||||
);
|
||||
@@ -93,8 +64,6 @@ let
|
||||
inherit (route) port;
|
||||
in
|
||||
{
|
||||
inherit host entry;
|
||||
registryName = "route";
|
||||
name = route.domain;
|
||||
value = {
|
||||
forceSSL = !isHttp;
|
||||
@@ -108,15 +77,6 @@ let
|
||||
|
||||
mkBackend = address: port: "http://${address}:${toString port}";
|
||||
|
||||
generatedHttpVirtualHostList = lib.concatLists (
|
||||
lib.mapAttrsToList (
|
||||
host: routes:
|
||||
lib.mapAttrsToList (entry: _route: mkVirtualHost host entry "http") (
|
||||
lib.filterAttrs (_entry: route: route.protocol == "http") routes
|
||||
)
|
||||
) registry.route
|
||||
);
|
||||
|
||||
mkRegistryPath =
|
||||
registryName: host: entry:
|
||||
"registry.${registryName}.${host}.${entry}";
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
};
|
||||
|
||||
serviceName = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
type = lib.types.strMatching "[a-z0-9][a-z0-9-]*";
|
||||
example = "valheim";
|
||||
description = "Name of the systemd service that runs the game server.";
|
||||
description = "Lowercase systemd service basename for the game server.";
|
||||
};
|
||||
|
||||
appId = lib.mkOption {
|
||||
@@ -87,7 +87,7 @@
|
||||
prepareScript = pkgs.writeShellScript "steamcmd-${service.serviceName}-prepare" ''
|
||||
set -eu
|
||||
|
||||
${pkgs.coreutils}/bin/install -d \
|
||||
${lib.getExe' pkgs.coreutils "install"} -d \
|
||||
-o ${user} \
|
||||
-g ${group} \
|
||||
-m 0770 \
|
||||
@@ -155,7 +155,7 @@
|
||||
wantedBy = lib.mkIf service.autoStart [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${pkgs.systemd}/bin/systemctl --no-block start ${service.serviceName}.service";
|
||||
ExecStart = "${lib.getExe' pkgs.systemd "systemctl"} --no-block start ${service.serviceName}.service";
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -5,14 +5,11 @@
|
||||
...
|
||||
}:
|
||||
# Bazel for the primary user's shell environment.
|
||||
let
|
||||
user = config.user.name;
|
||||
in
|
||||
{
|
||||
options.modules.shell.bazel.enable =
|
||||
lib.mkEnableOption "Bazel for the primary user's shell environment";
|
||||
|
||||
config = lib.mkIf config.modules.shell.bazel.enable {
|
||||
home-manager.users.${user}.home.packages = [ pkgs.bazel ];
|
||||
home-manager.users.${config.user.name}.home.packages = [ pkgs.bazel ];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
# direnv for the primary user, configured through home-manager.
|
||||
let
|
||||
user = config.user.name;
|
||||
in
|
||||
{
|
||||
options.modules.shell.direnv.enable =
|
||||
lib.mkEnableOption "direnv for the primary user, with nix-direnv shell caching";
|
||||
|
||||
config = lib.mkIf config.modules.shell.direnv.enable {
|
||||
home-manager.users.${user}.programs.direnv = {
|
||||
enable = true;
|
||||
silent = true;
|
||||
|
||||
# nix-direnv caches the evaluated shell so re-entering a directory is instant
|
||||
# instead of re-running `nix develop`, and adds the `use flake` stdlib helper.
|
||||
nix-direnv.enable = true;
|
||||
stdlib = builtins.readFile ./repo-flake.sh;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
if declare -f use_flake >/dev/null; then
|
||||
__dotfiles_use_flake_definition="$(declare -f use_flake)"
|
||||
eval "${__dotfiles_use_flake_definition/use_flake/__dotfiles_nix_direnv_use_flake}"
|
||||
unset __dotfiles_use_flake_definition
|
||||
|
||||
use_flake() {
|
||||
local flake_expr="${1:-.}"
|
||||
|
||||
if [[ "$flake_expr" == //* ]]; then
|
||||
local git_root
|
||||
if ! git_root="$(git rev-parse --show-toplevel 2>/dev/null)"; then
|
||||
log_error 'use flake // requires a Git repository'
|
||||
return 1
|
||||
fi
|
||||
|
||||
local flake_suffix="${flake_expr#//}"
|
||||
if [[ -n "$flake_suffix" && "$flake_suffix" != \#* && "$flake_suffix" != /* ]]; then
|
||||
flake_expr="path:${git_root}/${flake_suffix}"
|
||||
else
|
||||
flake_expr="path:${git_root}${flake_suffix}"
|
||||
fi
|
||||
|
||||
shift
|
||||
__dotfiles_nix_direnv_use_flake "$flake_expr" "$@"
|
||||
return
|
||||
fi
|
||||
|
||||
__dotfiles_nix_direnv_use_flake "$@"
|
||||
}
|
||||
else
|
||||
log_error 'nix-direnv must define use_flake before the // wrapper loads'
|
||||
return 1
|
||||
fi
|
||||
@@ -6,7 +6,7 @@
|
||||
}:
|
||||
# fish for the primary user, configured through home-manager.
|
||||
let
|
||||
cfg = config.modules.shell.fish; # NOLINT(let_order)
|
||||
cfg = config.modules.shell.fish;
|
||||
commandsOf = shortcuts: lib.mapAttrs (_name: shortcut: shortcut.command) shortcuts;
|
||||
shortcuts = lib.attrByPath [ config.networking.hostName ] { } config.registry.shell-shortcut;
|
||||
user = config.user.name;
|
||||
|
||||
@@ -12,18 +12,14 @@ let
|
||||
piDir = "${config.users.users.${user}.home}/.pi/agent";
|
||||
in
|
||||
{
|
||||
options.modules.shell.pi = {
|
||||
enable = lib.mkEnableOption "Pi, a terminal coding agent, configured through home-manager";
|
||||
};
|
||||
options.modules.shell.pi.enable =
|
||||
lib.mkEnableOption "Pi, a terminal coding agent, configured through home-manager";
|
||||
|
||||
config = lib.mkIf config.modules.shell.pi.enable {
|
||||
home-manager.users.${user} = {
|
||||
programs.pi-coding-agent = {
|
||||
enable = true;
|
||||
package = pkgs.pi-coding-agent.overrideAttrs {
|
||||
src = ../../../../../third_party/pi;
|
||||
patches = [ ];
|
||||
};
|
||||
package = pkgs.alexion.pi-coding-agent;
|
||||
|
||||
settings = {
|
||||
defaultProvider = "openai-codex";
|
||||
|
||||
@@ -13,9 +13,8 @@ in
|
||||
lib.mkEnableOption "Python tools for the primary user's shell environment";
|
||||
|
||||
config = lib.mkIf config.modules.shell.python.enable {
|
||||
home-manager.users.${user}.home = {
|
||||
sessionVariables.PYTHON_TOOLCHAIN_ROOT = "${pkgs.python3}";
|
||||
packages = [ pkgs.python3 ];
|
||||
};
|
||||
registry.env.${config.networking.hostName}.PYTHON_TOOLCHAIN_ROOT = "${pkgs.python3}";
|
||||
|
||||
home-manager.users.${user}.home.packages = [ pkgs.python3 ];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,9 +23,9 @@ in
|
||||
lib.mkEnableOption "Rust tools for the primary user's shell environment";
|
||||
|
||||
config = lib.mkIf config.modules.shell.rust.enable {
|
||||
home-manager.users.${user} = {
|
||||
home.sessionVariables.RUST_TOOLCHAIN_ROOT = "${toolchain}";
|
||||
registry.env.${config.networking.hostName}.RUST_TOOLCHAIN_ROOT = "${toolchain}";
|
||||
|
||||
home-manager.users.${user} = {
|
||||
home.packages = [
|
||||
toolchain
|
||||
pkgs.rust-analyzer
|
||||
|
||||
4
configs/nixos/registry/env.nix
Normal file
4
configs/nixos/registry/env.nix
Normal file
@@ -0,0 +1,4 @@
|
||||
{ lib, ... }:
|
||||
{
|
||||
type = lib.types.str;
|
||||
}
|
||||
112
configs/nixos/registry/keybind.nix
Normal file
112
configs/nixos/registry/keybind.nix
Normal file
@@ -0,0 +1,112 @@
|
||||
{ lib, util, ... }:
|
||||
{
|
||||
checks = [
|
||||
{
|
||||
assertion = util.registry.predicates.uniquePerHost (entry: entry.value.keys);
|
||||
message = "Desktop keybinds must use unique key chords per host.";
|
||||
}
|
||||
{
|
||||
assertion =
|
||||
entries:
|
||||
lib.all (
|
||||
entry:
|
||||
(entry.value.action.kind == "exec" && entry.value.action.command != null)
|
||||
|| (
|
||||
builtins.elem entry.value.action.kind [
|
||||
"focus"
|
||||
"move-window"
|
||||
]
|
||||
&& lib.xor (entry.value.action.direction != null) (entry.value.action.workspace != null)
|
||||
)
|
||||
|| (
|
||||
entry.value.action.kind == "resize-window"
|
||||
&& entry.value.action.direction != null
|
||||
&& entry.value.action.workspace == null
|
||||
)
|
||||
|| (
|
||||
builtins.elem entry.value.action.kind [
|
||||
"toggle-floating"
|
||||
"toggle-fullscreen"
|
||||
"close-window"
|
||||
]
|
||||
&& entry.value.action.command == null
|
||||
&& entry.value.action.direction == null
|
||||
&& entry.value.action.workspace == null
|
||||
)
|
||||
) entries;
|
||||
message = "Desktop keybind actions must set the fields their kind requires.";
|
||||
}
|
||||
];
|
||||
|
||||
type = lib.types.submodule {
|
||||
options = {
|
||||
keys = lib.mkOption {
|
||||
description = "Sets the key chord for this desktop keybind.";
|
||||
type = lib.types.str;
|
||||
example = "SUPER + Return";
|
||||
};
|
||||
|
||||
repeating = lib.mkOption {
|
||||
description = "Repeats the keybind action while the key chord is held.";
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
example = true;
|
||||
};
|
||||
|
||||
locked = lib.mkOption {
|
||||
description = "Allows the keybind action while the session is locked.";
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
example = true;
|
||||
};
|
||||
|
||||
action = lib.mkOption {
|
||||
description = "Declares the action this keybind runs.";
|
||||
type = lib.types.submodule {
|
||||
options = {
|
||||
kind = lib.mkOption {
|
||||
description = "Selects the keybind action kind.";
|
||||
type = lib.types.enum [
|
||||
"exec"
|
||||
"focus"
|
||||
"move-window"
|
||||
"resize-window"
|
||||
"toggle-floating"
|
||||
"toggle-fullscreen"
|
||||
"close-window"
|
||||
];
|
||||
};
|
||||
|
||||
command = lib.mkOption {
|
||||
description = "Command executed by an exec keybind.";
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "ghostty";
|
||||
};
|
||||
|
||||
direction = lib.mkOption {
|
||||
description = "Direction targeted by a keybind action.";
|
||||
type = lib.types.nullOr (
|
||||
lib.types.enum [
|
||||
"down"
|
||||
"left"
|
||||
"right"
|
||||
"up"
|
||||
]
|
||||
);
|
||||
default = null;
|
||||
example = "left";
|
||||
};
|
||||
|
||||
workspace = lib.mkOption {
|
||||
description = "Workspace targeted by a keybind action.";
|
||||
type = lib.types.nullOr lib.types.int;
|
||||
default = null;
|
||||
example = 1;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
29
configs/nixos/registry/waybar.nix
Normal file
29
configs/nixos/registry/waybar.nix
Normal file
@@ -0,0 +1,29 @@
|
||||
{ lib, ... }:
|
||||
{
|
||||
type = lib.types.submodule {
|
||||
options = {
|
||||
position = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"left"
|
||||
"center"
|
||||
"right"
|
||||
];
|
||||
default = "right";
|
||||
description = "Selects the Waybar section that shows the module.";
|
||||
};
|
||||
|
||||
order = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 100;
|
||||
example = 50;
|
||||
description = "Sorts the module within its Waybar section.";
|
||||
};
|
||||
|
||||
settings = lib.mkOption {
|
||||
type = lib.types.attrsOf lib.types.anything;
|
||||
default = { };
|
||||
description = "Sets the Waybar module configuration.";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
19
configs/nixos/registry/window-icon.nix
Normal file
19
configs/nixos/registry/window-icon.nix
Normal file
@@ -0,0 +1,19 @@
|
||||
{ lib, ... }:
|
||||
{
|
||||
|
||||
type = lib.types.submodule {
|
||||
options = {
|
||||
class = lib.mkOption {
|
||||
description = "Matches the window class reported by the compositor.";
|
||||
type = lib.types.str;
|
||||
example = ".*[Ff]irefox.*";
|
||||
};
|
||||
|
||||
icon = lib.mkOption {
|
||||
description = "Sets the icon shown for matching windows.";
|
||||
type = lib.types.str;
|
||||
example = "";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
2
docs/.obsidian/app.json
vendored
2
docs/.obsidian/app.json
vendored
@@ -5,7 +5,7 @@
|
||||
"livePreview": false,
|
||||
"readableLineLength": false,
|
||||
"strictLineBreaks": true,
|
||||
"propertiesInDocument": "hidden",
|
||||
"propertiesInDocument": "visible",
|
||||
"showLineNumber": true,
|
||||
"autoPairMarkdown": false,
|
||||
"useTab": false,
|
||||
|
||||
3
docs/.obsidian/community-plugins.json
vendored
3
docs/.obsidian/community-plugins.json
vendored
@@ -5,5 +5,6 @@
|
||||
"obsidian-style-settings",
|
||||
"custom-font-loader",
|
||||
"obsidian-icon-folder",
|
||||
"table-editor-obsidian"
|
||||
"table-editor-obsidian",
|
||||
"obsidian-linter"
|
||||
]
|
||||
16
docs/.obsidian/graph.json
vendored
16
docs/.obsidian/graph.json
vendored
@@ -28,6 +28,20 @@
|
||||
"rgb": 5366249
|
||||
}
|
||||
},
|
||||
{
|
||||
"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\" ",
|
||||
"color": {
|
||||
@@ -60,6 +74,6 @@
|
||||
"repelStrength": 20,
|
||||
"linkStrength": 1,
|
||||
"linkDistance": 250,
|
||||
"scale": 0.3645607001522557,
|
||||
"scale": 0.24304046676817048,
|
||||
"close": false
|
||||
}
|
||||
326
docs/.obsidian/plugins/obsidian-linter/data.json
vendored
Normal file
326
docs/.obsidian/plugins/obsidian-linter/data.json
vendored
Normal file
@@ -0,0 +1,326 @@
|
||||
{
|
||||
"ruleConfigs": {
|
||||
"add-blank-line-after-yaml": {
|
||||
"enabled": true
|
||||
},
|
||||
"dedupe-yaml-array-values": {
|
||||
"enabled": true,
|
||||
"dedupe-alias-key": true,
|
||||
"dedupe-tag-key": true,
|
||||
"dedupe-array-keys": true,
|
||||
"ignore-keys": []
|
||||
},
|
||||
"escape-yaml-special-characters": {
|
||||
"enabled": true,
|
||||
"try-to-escape-single-line-arrays": true
|
||||
},
|
||||
"force-yaml-escape": {
|
||||
"enabled": true,
|
||||
"force-yaml-escape-keys": []
|
||||
},
|
||||
"format-tags-in-yaml": {
|
||||
"enabled": true
|
||||
},
|
||||
"format-yaml-array": {
|
||||
"enabled": true,
|
||||
"alias-key": true,
|
||||
"tag-key": true,
|
||||
"default-array-keys": true,
|
||||
"force-single-line-array-style": [],
|
||||
"force-multi-line-array-style": []
|
||||
},
|
||||
"insert-yaml-attributes": {
|
||||
"enabled": true,
|
||||
"text-to-insert": []
|
||||
},
|
||||
"move-inline-fields-to-yaml": {
|
||||
"enabled": true,
|
||||
"how-to-handle-full-line-fields": "Move and remove",
|
||||
"how-to-handle-bracketed-fields": "Leave in place",
|
||||
"how-to-handle-existing-keys": "Skip",
|
||||
"inline-keys-to-ignore": []
|
||||
},
|
||||
"move-tags-to-yaml": {
|
||||
"enabled": true,
|
||||
"how-to-handle-existing-tags": "Nothing",
|
||||
"tags-to-ignore": []
|
||||
},
|
||||
"remove-yaml-keys": {
|
||||
"enabled": true,
|
||||
"yaml-keys-to-remove": []
|
||||
},
|
||||
"sort-yaml-array-values": {
|
||||
"enabled": true,
|
||||
"sort-alias-key": true,
|
||||
"sort-tag-key": true,
|
||||
"sort-array-keys": true,
|
||||
"ignore-keys": [],
|
||||
"sort-order": "Ascending Alphabetical"
|
||||
},
|
||||
"yaml-key-sort": {
|
||||
"enabled": true,
|
||||
"yaml-key-priority-sort-order": [
|
||||
"aliases",
|
||||
"date-created",
|
||||
"date-modified"
|
||||
],
|
||||
"priority-keys-at-start-of-yaml": true,
|
||||
"yaml-sort-order-for-other-keys": "None"
|
||||
},
|
||||
"yaml-timestamp": {
|
||||
"enabled": true,
|
||||
"date-created": true,
|
||||
"date-created-key": "date-created",
|
||||
"date-created-source-of-truth": "file system",
|
||||
"date-modified": true,
|
||||
"date-modified-key": "date-modified",
|
||||
"date-modified-source-of-truth": "file system",
|
||||
"format": "YYYY-MM-DD HH:mm",
|
||||
"convert-to-utc": false,
|
||||
"update-on-file-contents-updated": "after 5 seconds"
|
||||
},
|
||||
"yaml-title": {
|
||||
"enabled": false,
|
||||
"title-key": "title",
|
||||
"mode": "first-h1-or-filename-if-h1-missing"
|
||||
},
|
||||
"yaml-title-alias": {
|
||||
"enabled": true,
|
||||
"preserve-existing-alias-section-style": true,
|
||||
"keep-alias-that-matches-the-filename": false,
|
||||
"use-yaml-key-to-keep-track-of-old-filename-or-heading": false,
|
||||
"alias-helper-key": "linter-yaml-title-alias",
|
||||
"remove-alias-if-empty": true
|
||||
},
|
||||
"capitalize-headings": {
|
||||
"enabled": true,
|
||||
"style": "Title Case",
|
||||
"ignore-case-words": [],
|
||||
"lowercase-words": [],
|
||||
"starting-word-ignore-characters": "'\"(‘“-",
|
||||
"ending-word-ignore-characters": ".?!,:;'\")”’0123456789-",
|
||||
"ignore-words": []
|
||||
},
|
||||
"file-name-heading": {
|
||||
"enabled": true
|
||||
},
|
||||
"header-increment": {
|
||||
"enabled": true,
|
||||
"start-at-h2": false
|
||||
},
|
||||
"headings-start-line": {
|
||||
"enabled": true
|
||||
},
|
||||
"remove-trailing-punctuation-in-heading": {
|
||||
"enabled": true,
|
||||
"punctuation-to-remove": ".,;:!。,;:!"
|
||||
},
|
||||
"footnote-after-punctuation": {
|
||||
"enabled": true
|
||||
},
|
||||
"move-footnotes-to-the-bottom": {
|
||||
"enabled": true,
|
||||
"include-blank-line-between-footnotes": false
|
||||
},
|
||||
"re-index-footnotes": {
|
||||
"enabled": true
|
||||
},
|
||||
"auto-correct-common-misspellings": {
|
||||
"enabled": true,
|
||||
"ignore-words": [],
|
||||
"skip-words-with-multiple-capitals": true,
|
||||
"extra-auto-correct-files": []
|
||||
},
|
||||
"blockquote-style": {
|
||||
"enabled": true,
|
||||
"style": "space"
|
||||
},
|
||||
"convert-bullet-list-markers": {
|
||||
"enabled": true
|
||||
},
|
||||
"default-language-for-code-fences": {
|
||||
"enabled": true,
|
||||
"default-language": ""
|
||||
},
|
||||
"emphasis-style": {
|
||||
"enabled": true,
|
||||
"style": "consistent"
|
||||
},
|
||||
"no-bare-urls": {
|
||||
"enabled": true,
|
||||
"no-bare-uris": true
|
||||
},
|
||||
"ordered-list-style": {
|
||||
"enabled": true,
|
||||
"number-style": "ascending",
|
||||
"list-end-style": ".",
|
||||
"preserve-start": false
|
||||
},
|
||||
"proper-ellipsis": {
|
||||
"enabled": true
|
||||
},
|
||||
"quote-style": {
|
||||
"enabled": true,
|
||||
"single-quote-enabled": true,
|
||||
"single-quote-style": "''",
|
||||
"double-quote-enabled": true,
|
||||
"double-quote-style": "\"\""
|
||||
},
|
||||
"remove-consecutive-list-markers": {
|
||||
"enabled": true
|
||||
},
|
||||
"remove-empty-list-markers": {
|
||||
"enabled": true
|
||||
},
|
||||
"remove-hyphenated-line-breaks": {
|
||||
"enabled": true
|
||||
},
|
||||
"remove-multiple-spaces": {
|
||||
"enabled": true
|
||||
},
|
||||
"strong-style": {
|
||||
"enabled": true,
|
||||
"style": "consistent"
|
||||
},
|
||||
"two-spaces-between-lines-with-content": {
|
||||
"enabled": false,
|
||||
"line-break-indicator": " "
|
||||
},
|
||||
"unordered-list-style": {
|
||||
"enabled": true,
|
||||
"list-style": "consistent"
|
||||
},
|
||||
"align-table-columns": {
|
||||
"enabled": true
|
||||
},
|
||||
"compact-yaml": {
|
||||
"enabled": true,
|
||||
"inner-new-lines": false
|
||||
},
|
||||
"consecutive-blank-lines": {
|
||||
"enabled": true
|
||||
},
|
||||
"convert-spaces-to-tabs": {
|
||||
"enabled": true,
|
||||
"tabsize": 4
|
||||
},
|
||||
"empty-line-around-blockquotes": {
|
||||
"enabled": true
|
||||
},
|
||||
"empty-line-around-code-fences": {
|
||||
"enabled": true
|
||||
},
|
||||
"empty-line-around-horizontal-rules": {
|
||||
"enabled": true
|
||||
},
|
||||
"empty-line-around-math-blocks": {
|
||||
"enabled": true
|
||||
},
|
||||
"empty-line-around-tables": {
|
||||
"enabled": true
|
||||
},
|
||||
"heading-blank-lines": {
|
||||
"enabled": true,
|
||||
"bottom": true,
|
||||
"empty-line-after-yaml": true
|
||||
},
|
||||
"line-break-at-document-end": {
|
||||
"enabled": true
|
||||
},
|
||||
"move-math-block-indicators-to-their-own-line": {
|
||||
"enabled": true
|
||||
},
|
||||
"paragraph-blank-lines": {
|
||||
"enabled": true
|
||||
},
|
||||
"remove-empty-lines-between-list-markers-and-checklists": {
|
||||
"enabled": true
|
||||
},
|
||||
"remove-link-spacing": {
|
||||
"enabled": true
|
||||
},
|
||||
"remove-space-around-characters": {
|
||||
"enabled": true,
|
||||
"include-fullwidth-forms": true,
|
||||
"include-cjk-symbols-and-punctuation": true,
|
||||
"include-dashes": true,
|
||||
"other-symbols": ""
|
||||
},
|
||||
"remove-space-before-or-after-characters": {
|
||||
"enabled": true,
|
||||
"characters-to-remove-space-before": ",!?;:).’”]",
|
||||
"characters-to-remove-space-after": "¿¡‘“(["
|
||||
},
|
||||
"space-after-list-markers": {
|
||||
"enabled": true
|
||||
},
|
||||
"space-between-chinese-japanese-or-korean-and-english-or-numbers": {
|
||||
"enabled": true,
|
||||
"english-symbols-punctuation-before": "-+;:'\"°%$)]",
|
||||
"english-symbols-punctuation-after": "-+'\"([¥$"
|
||||
},
|
||||
"trailing-spaces": {
|
||||
"enabled": true,
|
||||
"two-space-line-break": false
|
||||
},
|
||||
"add-blockquote-indentation-on-paste": {
|
||||
"enabled": true
|
||||
},
|
||||
"prevent-double-checklist-indicator-on-paste": {
|
||||
"enabled": true
|
||||
},
|
||||
"prevent-double-list-item-indicator-on-paste": {
|
||||
"enabled": true
|
||||
},
|
||||
"proper-ellipsis-on-paste": {
|
||||
"enabled": true
|
||||
},
|
||||
"remove-hyphens-on-paste": {
|
||||
"enabled": true
|
||||
},
|
||||
"remove-leading-or-trailing-whitespace-on-paste": {
|
||||
"enabled": true
|
||||
},
|
||||
"remove-leftover-footnotes-from-quote-on-paste": {
|
||||
"enabled": true
|
||||
},
|
||||
"remove-multiple-blank-lines-on-paste": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"lintOnSave": true,
|
||||
"recordLintOnSaveLogs": false,
|
||||
"displayChanged": true,
|
||||
"suppressMessageWhenNoChange": true,
|
||||
"enableDiffPreviewView": true,
|
||||
"suppressLintAllFilesConfirmationModal": false,
|
||||
"suppressLintAllFilesInFolderConfirmationModal": false,
|
||||
"lintOnFileChange": false,
|
||||
"displayLintOnFileChangeNotice": false,
|
||||
"settingsConvertedToConfigKeyValues": true,
|
||||
"textAreaSettingsConvertedToListItemSettings": true,
|
||||
"defaultArrayStyleMovedToCommonStyles": true,
|
||||
"additionalFileExtensions": [],
|
||||
"foldersToIgnore": [
|
||||
"99 meta/templates"
|
||||
],
|
||||
"filesToIgnore": [
|
||||
{
|
||||
"label": "todo",
|
||||
"match": "TODO.md",
|
||||
"flags": "i"
|
||||
}
|
||||
],
|
||||
"linterLocale": "system-default",
|
||||
"logLevel": "ERROR",
|
||||
"lintCommands": [],
|
||||
"customRegexes": [],
|
||||
"commonStyles": {
|
||||
"aliasArrayStyle": "multi-line",
|
||||
"tagArrayStyle": "single-line",
|
||||
"defaultArrayStyle": "multi-line",
|
||||
"minimumNumberOfDollarSignsToBeAMathBlock": 2,
|
||||
"escapeCharacter": "\"",
|
||||
"removeUnnecessaryEscapeCharsForMultiLineArrays": true
|
||||
}
|
||||
}
|
||||
35147
docs/.obsidian/plugins/obsidian-linter/default-misspellings.md
vendored
Normal file
35147
docs/.obsidian/plugins/obsidian-linter/default-misspellings.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
399
docs/.obsidian/plugins/obsidian-linter/main.js
vendored
Normal file
399
docs/.obsidian/plugins/obsidian-linter/main.js
vendored
Normal file
File diff suppressed because one or more lines are too long
11
docs/.obsidian/plugins/obsidian-linter/manifest.json
vendored
Normal file
11
docs/.obsidian/plugins/obsidian-linter/manifest.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"id": "obsidian-linter",
|
||||
"name": "Linter",
|
||||
"version": "1.34.1",
|
||||
"minAppVersion": "1.13.0",
|
||||
"description": "Formats and styles your notes. It can be used to format YAML tags, aliases, arrays, and metadata; footnotes; headings; spacing; math blocks; regular markdown contents like list, italics, and bold styles; and more with the use of custom rule options as well.",
|
||||
"author": "Victor Tao",
|
||||
"authorUrl": "https://github.com/platers",
|
||||
"helpUrl": "https://platers.github.io/obsidian-linter/",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
1
docs/.obsidian/plugins/obsidian-linter/styles.css
vendored
Normal file
1
docs/.obsidian/plugins/obsidian-linter/styles.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.linter-no-border{border:none}.linter-border-bottom{border-bottom:1px solid var(--background-modifier-border);border-top:0;margin-bottom:.75em}.custom-row-description{margin-top:0}.modal-warn,.search-zero-state{font-weight:700}.modal-heading,.search-zero-state{text-align:center}.confirm-modal-checkbox-container{align-items:center;display:flex;gap:6px;margin-top:8px}.disabled-list-entry{color:var(--text-faint)}.linter-diff-preview-view{display:flex;flex-direction:column;height:100%;min-width:0;overflow:hidden}.linter-diff-preview-empty{color:var(--text-muted);padding:16px}.linter-diff-preview-header{align-items:flex-start;border-bottom:1px solid var(--background-modifier-border);display:flex;flex:0 0 auto;gap:12px;justify-content:space-between;padding:12px 14px}.linter-diff-preview-title-group{min-width:0}.linter-diff-preview-header h3{font-size:var(--font-ui-medium);line-height:1.3;margin:0 0 6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.linter-diff-preview-actions{display:flex;flex:0 0 auto;gap:8px}.linter-diff-preview-body{flex:1 1 auto;min-height:0;overflow:auto;padding:12px 14px 16px}.linter-diff-preview-view .linter-diff-summary{color:var(--text-normal);display:inline-block;font-size:var(--font-ui-small);margin:0 0 10px;padding:5px 8px}.linter-diff-preview-view .linter-diff,.linter-diff-preview-view .linter-diff-summary{background:var(--background-secondary);border:1px solid var(--background-modifier-border);border-radius:6px}.linter-diff-preview-view .linter-diff{font-family:var(--font-monospace);font-size:var(--font-smaller);line-height:1.45;margin:0;overflow:hidden;padding:6px 0}.linter-diff-preview-view .linter-diff-line{display:grid;grid-template-columns:26px minmax(0,1fr);min-height:20px;padding:1px 10px 1px 0}.linter-diff-preview-view .linter-diff-marker{color:var(--text-muted);padding-right:8px;text-align:right;user-select:none}.linter-diff-preview-view .linter-diff-text{min-width:0;overflow-wrap:anywhere;white-space:pre-wrap}.linter-diff-preview-view .linter-diff-skip{border-bottom:1px solid var(--background-modifier-border);border-top:1px solid var(--background-modifier-border);color:var(--text-faint);font-family:var(--font-interface);font-size:var(--font-ui-smaller);padding:4px 12px;text-align:center}.linter-diff-preview-view .linter-diff-added{background:rgba(var(--color-green-rgb),.12);color:var(--text-normal)}.linter-diff-preview-view .linter-diff-removed{background:rgba(var(--color-red-rgb),.12);color:var(--text-normal)}.linter-diff-preview-view .linter-diff-context{color:var(--text-muted)}
|
||||
84
docs/.obsidian/workspace.json
vendored
84
docs/.obsidian/workspace.json
vendored
@@ -4,21 +4,21 @@
|
||||
"type": "split",
|
||||
"children": [
|
||||
{
|
||||
"id": "6d9088d392cb8604",
|
||||
"id": "81c4860f495113bf",
|
||||
"type": "tabs",
|
||||
"children": [
|
||||
{
|
||||
"id": "22a4c1f201a1b00e",
|
||||
"id": "94a7b5a014888797",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "markdown",
|
||||
"state": {
|
||||
"file": "00 INBOX/ai/valheim-items.md",
|
||||
"mode": "preview",
|
||||
"file": "03 wrk/tasks/0002-nixos-refactor.md",
|
||||
"mode": "source",
|
||||
"source": true
|
||||
},
|
||||
"icon": "lucide-file",
|
||||
"title": "valheim-items"
|
||||
"title": "0002-nixos-refactor"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -91,7 +91,7 @@
|
||||
"state": {
|
||||
"type": "localgraph",
|
||||
"state": {
|
||||
"file": "00 INBOX/ai/valheim-items.md",
|
||||
"file": "03 wrk/tasks/0002-nixos-refactor.md",
|
||||
"options": {
|
||||
"collapse-filter": true,
|
||||
"search": "",
|
||||
@@ -115,7 +115,7 @@
|
||||
"linkStrength": 1,
|
||||
"linkDistance": 250,
|
||||
"scale": 0.6666666666666666,
|
||||
"close": true
|
||||
"close": false
|
||||
}
|
||||
},
|
||||
"icon": "lucide-git-fork",
|
||||
@@ -184,20 +184,6 @@
|
||||
"title": "tag"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "8918bf52a32e4a8e",
|
||||
"type": "leaf",
|
||||
"state": {
|
||||
"type": "all-properties",
|
||||
"state": {
|
||||
"sortOrder": "frequency",
|
||||
"showSearch": false,
|
||||
"searchQuery": ""
|
||||
},
|
||||
"icon": "lucide-ghost",
|
||||
"title": "all-properties"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "c10908b3bd97f5fa",
|
||||
"type": "leaf",
|
||||
@@ -231,43 +217,43 @@
|
||||
"table-editor-obsidian:Advanced Tables Toolbar": false
|
||||
}
|
||||
},
|
||||
"active": "22a4c1f201a1b00e",
|
||||
"active": "94a7b5a014888797",
|
||||
"lastOpenFiles": [
|
||||
"00 INBOX/ai/hyprland-lua-migration-plan.md",
|
||||
"00 INBOX/ai/deadnix-architecture.md",
|
||||
"00 INBOX/ai/valheim-items.md",
|
||||
"TODO.md",
|
||||
"AGENTS.md",
|
||||
"00 INBOX/valheim progression.md",
|
||||
"technical-writing.md",
|
||||
"rust-module-prototype.md",
|
||||
"00 INBOX/ai/statix-architecture.md",
|
||||
"00 INBOX/ai/nix-statix-deadnix-rules.md",
|
||||
"03 wrk/productivity.md",
|
||||
"03 wrk/tasks/0003-whisper-dictation.md",
|
||||
"03 wrk/tasks/0001-todo-cli.md",
|
||||
"03 wrk/tasks/0002-nixos-refactor.md",
|
||||
"03 wrk/dotfiles.md",
|
||||
"03 wrk/machoke.md",
|
||||
"03 wrk/falinks.md",
|
||||
"03 wrk/ariadne.md",
|
||||
"99 meta/templates/task.md",
|
||||
"03 wrk/tasks/0005-modules-services-navidrome.md",
|
||||
"03 wrk/tasks/0003-registry-homepage.md",
|
||||
"03 wrk/tasks/0039-hosts-zeus-hardware-configuration.md",
|
||||
"03 wrk/tasks/0030-dotfiles-hosts.md",
|
||||
"03 wrk/tasks/0038-hosts-zeus-disk.md",
|
||||
"03 wrk/tasks/0037-hosts-zeus-default.md",
|
||||
"03 wrk/tasks/0036-hosts-pikachu-hardware-configuration.md",
|
||||
"03 wrk/tasks/0035-hosts-pikachu-disk.md",
|
||||
"03 wrk/tasks/0034-hosts-pikachu-default.md",
|
||||
"03 wrk/tasks/0033-hosts-neogaia-hardware-configuration.md",
|
||||
"03 wrk/tasks/0032-hosts-neogaia-disk.md",
|
||||
"03 wrk/tasks/0031-hosts-neogaia-default.md",
|
||||
"03 wrk/tasks/0029-guests-valheim.md",
|
||||
"03 wrk/tasks/0028-guests-reverse-proxy.md",
|
||||
"03 wrk/tasks/0027-guests-palworld.md",
|
||||
"03 wrk/tasks/0026-guests-navidrome.md",
|
||||
"03 wrk/tasks",
|
||||
"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~",
|
||||
"TODO.md~",
|
||||
"00 INBOX/ai/xatu-rule-implementation-order.md",
|
||||
"00 INBOX/ai/xatu-architecture-design.md",
|
||||
"00 INBOX/ai/nix-style-linter-plan.md",
|
||||
"00 INBOX/ai/nix-style-guide-agent-customization.md",
|
||||
"00 INBOX/ai/game-guest-readiness-plan.md",
|
||||
"00 INBOX/ai/xatu-node-rule-static-dispatch-plan.md",
|
||||
"00 INBOX/ai/nix-style-linter-pr-1-vendor-rnix.md",
|
||||
"00 INBOX/ai/nix-style-linter-pr-3-let-order.md",
|
||||
"00 INBOX/ai/nix-style-linter-pr-2-scaffold-cli.md",
|
||||
"00 INBOX/ai/homepage-pikachu-setup-plan.md",
|
||||
"00 INBOX/ai/source-driven-remote-deploy-plan.md",
|
||||
"00 INBOX/ai/monorepo-work-report.md",
|
||||
"00 INBOX/ai/clew-ast-exploration.md",
|
||||
"00 INBOX/ai/steamcmd servers.md",
|
||||
"00 INBOX/ai/clew-ast-exploration.md~",
|
||||
"03 wrk/machoke.md~",
|
||||
"03 wrk/falinks.md~",
|
||||
"99 meta/attachments/bases/unlinked mentions.base",
|
||||
"00 INBOX/ai",
|
||||
"03 wrk/ai",
|
||||
"99 meta/Untitled"
|
||||
"03 wrk/ai"
|
||||
]
|
||||
}
|
||||
@@ -1,304 +0,0 @@
|
||||
# Deadnix Architecture
|
||||
|
||||
## Purpose
|
||||
|
||||
This note explains how `deadnix` finds unused Nix code.
|
||||
It focuses on architecture, not whether each check belongs in our linter.
|
||||
|
||||
Use this note when deciding whether our linter needs scope analysis.
|
||||
|
||||
## What Deadnix Is
|
||||
|
||||
`deadnix` is a dead-code scanner for Nix.
|
||||
It finds bindings that introduce names which no live code uses.
|
||||
|
||||
It is not a general style linter.
|
||||
It does not have many independent rules.
|
||||
It implements one analysis family: unused declarations.
|
||||
|
||||
Like `statix`, it parses source text with `rnix`.
|
||||
It does not evaluate Nix.
|
||||
It does not import files.
|
||||
It reasons over local syntax and local scope only.
|
||||
|
||||
## Minimal Dead-Code Vocabulary
|
||||
|
||||
- A binding introduces a name.
|
||||
- A scope defines where introduced names are visible.
|
||||
- A use is an identifier occurrence that refers to a binding.
|
||||
- Shadowing happens when an inner scope reuses an outer name.
|
||||
- Dead code is a binding that no live code uses.
|
||||
- A fixed point is the final result after repeated analysis no longer changes.
|
||||
|
||||
Dead-code analysis needs more state than a simple syntax lint.
|
||||
It must know which names a scope introduces and where those names can be used.
|
||||
|
||||
## Component Map
|
||||
|
||||
`deadnix` has fewer layers than `statix`.
|
||||
The command-line entry point, file walk, parsing, analysis, reporting, and editing live in one small tool.
|
||||
|
||||
The important concepts are separate modules:
|
||||
|
||||
- settings control analysis options;
|
||||
- scopes describe where names are introduced;
|
||||
- bindings describe individual declared names;
|
||||
- usage search answers whether a name appears in a live body;
|
||||
- dead-code results connect a binding to its scope;
|
||||
- reports render findings;
|
||||
- edits remove or rename unused declarations.
|
||||
|
||||
## Main Pipeline
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Parse CLI options] --> B[Collect input files]
|
||||
B --> C[Read each file]
|
||||
C --> D[Parse with rnix]
|
||||
D --> E[Skip file on parse errors]
|
||||
D --> F[Run dead-code analysis]
|
||||
F --> G[Print reports unless quiet]
|
||||
G --> H[Apply edits when --edit is set]
|
||||
H --> I[Exit 1 when --fail and findings exist]
|
||||
```
|
||||
|
||||
The CLI accepts files or directories.
|
||||
Directory mode walks `.nix` files.
|
||||
Hidden files require an explicit option.
|
||||
Excludes filter paths before analysis.
|
||||
|
||||
The tool skips dead-code diagnostics when a file has parse errors.
|
||||
It reports parse errors to the user and continues with other files.
|
||||
|
||||
## Analysis Settings
|
||||
|
||||
The analysis has four important switches.
|
||||
|
||||
`no_lambda_arg` disables checks for single lambda arguments.
|
||||
This can avoid noise when an API requires an argument name.
|
||||
|
||||
`no_lambda_pattern_names` disables checks for names in attribute-set function patterns.
|
||||
This matters for `callPackage`-style code, where argument names can be external API.
|
||||
|
||||
`no_underscore` ignores bindings that start with `_`.
|
||||
This treats `_name` as an intentional unused placeholder.
|
||||
|
||||
`warn_used_underscore` reports underscore-prefixed bindings that code actually uses.
|
||||
This enforces the convention that `_name` means unused.
|
||||
|
||||
These settings are architecture-relevant because dead-code policy depends on API context.
|
||||
A repository linter needs a way to express that context.
|
||||
|
||||
## Scope Model
|
||||
|
||||
`deadnix` recognizes four scope types.
|
||||
|
||||
### Lambda pattern scope
|
||||
|
||||
A lambda pattern is a function argument written as an attribute set pattern.
|
||||
It can introduce field names and an optional alias for the whole argument set.
|
||||
|
||||
Example shape:
|
||||
|
||||
```nix
|
||||
args @ { pkgs, lib, ... }: body
|
||||
```
|
||||
|
||||
This scope can introduce `args`, `pkgs`, and `lib`.
|
||||
|
||||
### Lambda argument scope
|
||||
|
||||
A single lambda argument introduces one name.
|
||||
|
||||
Example shape:
|
||||
|
||||
```nix
|
||||
pkgs: body
|
||||
```
|
||||
|
||||
This scope introduces `pkgs`.
|
||||
|
||||
### Let scope
|
||||
|
||||
A `let` expression introduces bindings and inherited names.
|
||||
Those names are available in binding expressions and in the body.
|
||||
|
||||
Example shape:
|
||||
|
||||
```nix
|
||||
let
|
||||
inherit (pkgs) hello;
|
||||
value = hello;
|
||||
in
|
||||
value
|
||||
```
|
||||
|
||||
This scope introduces `hello` and `value`.
|
||||
|
||||
### Recursive attribute-set scope
|
||||
|
||||
A `rec` attribute set introduces names that sibling attributes can reference.
|
||||
`deadnix` models this scope, but treats its bindings as non-removable.
|
||||
|
||||
That conservative choice avoids deleting public-looking attributes from recursive sets.
|
||||
|
||||
## Binding Model
|
||||
|
||||
A binding stores three facts.
|
||||
|
||||
It stores the identifier name.
|
||||
It stores the syntax node that declared the name.
|
||||
It stores whether the binding is mortal.
|
||||
|
||||
A mortal binding can become a dead-code finding.
|
||||
A non-mortal binding participates in name lookup but will not be removed.
|
||||
|
||||
Single lambda arguments that already start with `_` are non-mortal.
|
||||
Recursive attribute-set bindings are also non-mortal.
|
||||
|
||||
A binding also checks for the `deadnix: skip` pragma.
|
||||
The pragma applies to a nearby following declaration.
|
||||
The scanner searches backward from the declaration and stops after a blank-line boundary.
|
||||
|
||||
## Usage Search
|
||||
|
||||
Usage search asks whether a given name appears inside a syntax subtree.
|
||||
|
||||
The search is scope-aware.
|
||||
When it enters a child scope that binds the same name, it stops following that branch.
|
||||
That prevents an inner binding from counting as a use of an outer binding.
|
||||
|
||||
Attribute paths get special handling.
|
||||
Ordinary attribute keys do not count as uses.
|
||||
Dynamic or string attribute names can contain expressions, so the search looks inside those parts.
|
||||
|
||||
`inherit` also gets special handling.
|
||||
A scope can inherit names from an outer scope.
|
||||
The analysis checks whether the inherited expression uses the searched name.
|
||||
|
||||
## Fixed-Point Analysis
|
||||
|
||||
`deadnix` repeats the scan until the result set stops changing.
|
||||
|
||||
This matters because dead code can depend on other dead code.
|
||||
A binding can appear used during the first pass because another binding references it.
|
||||
If the referring binding is dead, the referenced binding is also dead.
|
||||
|
||||
The loop tracks dead declaration nodes.
|
||||
During later scans, uses inside already-dead bodies do not keep other bindings alive.
|
||||
|
||||
This fixed-point loop is the central architectural difference from simple local syntax lints.
|
||||
It makes dead-code results transitive.
|
||||
|
||||
## Reporting Model
|
||||
|
||||
A result stores the scope and binding for one finding.
|
||||
The display text combines both parts.
|
||||
Examples include unused lambda pattern, unused lambda argument, and unused let binding.
|
||||
|
||||
Human output uses `ariadne`.
|
||||
The reporter groups ranges by line and labels the exact binding names.
|
||||
It uses different colors for different scope kinds.
|
||||
|
||||
JSON output exists behind a feature flag.
|
||||
It emits file, message, line, column, and end column.
|
||||
|
||||
`NO_COLOR` disables colored human output.
|
||||
|
||||
## Edit Model
|
||||
|
||||
`deadnix --edit` converts findings into text edits.
|
||||
Each edit has a start offset, end offset, and replacement string.
|
||||
|
||||
The edit behavior depends on scope kind.
|
||||
|
||||
For unused lambda pattern names, it removes the name and nearby comma or whitespace.
|
||||
For unused pattern aliases, it removes the alias syntax.
|
||||
For unused lambda arguments, it renames the argument with an underscore prefix.
|
||||
For unused `let` assignments, it deletes the assignment.
|
||||
For unused inherited names, it deletes only the inherited name.
|
||||
|
||||
After those edits, it parses the edited source again.
|
||||
Then it removes empty `let in` constructs and empty `inherit` statements.
|
||||
|
||||
This cleanup pass prevents the first edit batch from leaving invalid or useless scaffolding.
|
||||
|
||||
## Error Handling
|
||||
|
||||
If a path cannot be stated or read, the CLI reports the file error and continues.
|
||||
If a file has parse errors, the CLI reports those errors and skips dead-code analysis for that file.
|
||||
|
||||
The `--fail` flag changes exit status when findings exist.
|
||||
Without `--fail`, findings do not make the command fail.
|
||||
|
||||
This design separates report generation from CI policy.
|
||||
|
||||
## Architectural Strengths
|
||||
|
||||
The scope model is explicit.
|
||||
Each scope type declares which bindings it introduces and where those bindings are visible.
|
||||
|
||||
The fixed-point loop catches transitive dead code.
|
||||
A binding used only by dead code also becomes dead.
|
||||
|
||||
The usage search handles shadowing.
|
||||
That prevents common false positives in nested functions and nested `let` expressions.
|
||||
|
||||
The edit model is tied to analysis results.
|
||||
It knows whether a binding came from a lambda, pattern, `let`, or `inherit`.
|
||||
|
||||
The tool has few moving parts.
|
||||
That makes the analysis easy to follow compared to a general multi-rule linter.
|
||||
|
||||
## Architectural Limits
|
||||
|
||||
`deadnix` does not evaluate Nix.
|
||||
It cannot know which names external callers depend on.
|
||||
This matters for module arguments, package function arguments, and public attribute sets.
|
||||
|
||||
It has global switches rather than project-specific rule contexts.
|
||||
For example, all lambda pattern names can be checked or skipped.
|
||||
The tool does not know that one directory contains `callPackage` functions.
|
||||
|
||||
The analysis is local to one file.
|
||||
It cannot see that another file imports a function and relies on its argument names.
|
||||
|
||||
The fixer performs text edits with byte ranges.
|
||||
It must handle punctuation and whitespace carefully.
|
||||
Pattern edits are harder than simple assignment deletion.
|
||||
|
||||
The tool has one analysis family.
|
||||
It is not a framework for many unrelated rules.
|
||||
|
||||
## Lessons for Our Linter
|
||||
|
||||
Dead-code checks need a scope pass.
|
||||
A per-node rule interface is not enough when a diagnostic depends on name visibility.
|
||||
|
||||
Represent scopes explicitly.
|
||||
Each scope type should provide introduced bindings and visible bodies.
|
||||
|
||||
Track shadowing before reporting unused names.
|
||||
A text search for an identifier is not sufficient.
|
||||
|
||||
Use fixed-point analysis for transitive dead code.
|
||||
One pass misses names used only by dead declarations.
|
||||
|
||||
Separate policy from mechanics.
|
||||
The mechanics can find unused names.
|
||||
Repository policy decides whether module arguments, package arguments, or underscore names count.
|
||||
|
||||
Treat autofix as scope-specific.
|
||||
Removing a pattern name is not the same operation as deleting a `let` assignment.
|
||||
|
||||
Run cleanup after edits.
|
||||
Dead-code fixes can create empty `let` expressions and empty `inherit` statements.
|
||||
|
||||
## Open Design Questions for Our Linter
|
||||
|
||||
- Should dead-code analysis be part of `nix-style-linter`, or a separate command?
|
||||
- Should policy vary by path, file kind, or suppression comment?
|
||||
- Should recursive attribute-set members ever become removable findings?
|
||||
- Should underscore names mean ignored, or should used underscore names be errors?
|
||||
- Should the linter report unused names without offering autofix first?
|
||||
- Should cleanup rules run as ordinary rules or as a post-fix pass?
|
||||
@@ -1,102 +0,0 @@
|
||||
# Hyprland Lua Migration Plan
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The Hyprland compositor config uses Home Manager's hyprlang output.
|
||||
Hyprland deprecates hyprlang for the main compositor config.
|
||||
The repository needs to generate a Lua config without changing desktop behavior.
|
||||
|
||||
## Solution
|
||||
|
||||
Keep the config owned by Nix and Home Manager.
|
||||
Switch the Hyprland Home Manager config output to Lua.
|
||||
Convert only the settings whose shape changes under Lua.
|
||||
|
||||
This is not a desktop redesign.
|
||||
The change preserves the current monitors, binds, window rules, environment variables, and UWSM startup.
|
||||
|
||||
## User Stories
|
||||
|
||||
1. As the operator, I want Hyprland to load `hyprland.lua`, so that startup no longer warns about hyprlang deprecation.
|
||||
|
||||
2. As the operator, I want existing keyboard binds to keep working, so that the desktop behavior stays unchanged.
|
||||
|
||||
3. As the operator, I want host monitor layout to stay unchanged, so that the physical display setup does not move.
|
||||
|
||||
4. As the operator, I want session environment variables to stay in the compositor config, so that Wayland and cursor behavior stays unchanged.
|
||||
|
||||
5. As the operator, I want auxiliary module binds to keep working, so that launcher, lock, screenshot, clipboard, notifications, and recording flows stay usable.
|
||||
|
||||
## Implementation Decisions
|
||||
|
||||
1. Use Home Manager's Lua config generator.
|
||||
Set the Hyprland config type to Lua and keep package ownership unchanged.
|
||||
|
||||
2. Keep ordinary compositor options declarative in Nix.
|
||||
Move input, general, decoration, xwayland, animation, and layout settings into the Lua config structure.
|
||||
|
||||
3. Remove hyprlang-only variables from generated config.
|
||||
Keep modifier and terminal values as Nix locals or helper inputs.
|
||||
|
||||
4. Add minimal local helpers for Lua calls.
|
||||
Use helpers only for repeated bind, dispatcher, command, environment, and rule shapes.
|
||||
|
||||
5. Convert all bind strings to Lua bind call data.
|
||||
Preserve key combinations, dispatchers, commands, repeat behavior, and locked-session behavior.
|
||||
|
||||
6. Convert contributed binds in their owning modules.
|
||||
Keep each bind near the module that owns the tool it launches.
|
||||
|
||||
7. Convert monitor strings to Lua monitor records.
|
||||
Preserve output names, modes, positions, scale values, and rotation.
|
||||
|
||||
8. Convert the screenshot window rule to the Lua rule shape.
|
||||
Preserve the Satty class matcher and floating behavior.
|
||||
|
||||
9. Convert environment strings to Lua environment calls.
|
||||
Preserve the Wayland flag and cursor variables.
|
||||
|
||||
10. Delete or update comments that describe hyprlang syntax.
|
||||
Keep comments that explain behavior or non-obvious invariants.
|
||||
|
||||
## Testing Decisions
|
||||
|
||||
1. Run the repository Nix formatting and style checks for modified Nix files.
|
||||
|
||||
2. Evaluate or build the affected host configuration.
|
||||
This proves Home Manager accepts the Lua settings structure.
|
||||
|
||||
3. Confirm the generated config file uses Lua.
|
||||
The expected output is `hyprland.lua`, not `hyprland.conf`.
|
||||
|
||||
4. Verify Hyprland accepts the generated Lua config.
|
||||
Use Hyprland config verification when the installed build supports it.
|
||||
Otherwise reload the compositor and inspect errors.
|
||||
|
||||
5. Smoke test the changed desktop surface.
|
||||
Test one bind from each source area after reload.
|
||||
|
||||
6. Check the explicit monitor layout on the host that declares it.
|
||||
Confirm resolution, position, scale, and rotation stayed unchanged.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Do not split the config into Lua modules.
|
||||
- Do not hand-write a full Lua config.
|
||||
- Do not add new Hyprland behavior.
|
||||
- Do not change UWSM startup.
|
||||
- Do not redesign binds, monitor layout, or visual settings.
|
||||
- Do not change hyprlock, hypridle, waybar, mako, or rofi settings beyond their Hyprland binds.
|
||||
|
||||
## Further Notes
|
||||
|
||||
Home Manager documents `configType = "lua"` for Hyprland.
|
||||
It maps settings to `hl.*` calls.
|
||||
It supports `_args` for multi-argument calls.
|
||||
It supports raw Lua expressions with `lib.generators.mkLuaInline`.
|
||||
|
||||
The riskiest parts are binds, monitor declarations, and window rules.
|
||||
Those parts changed shape more than scalar config sections.
|
||||
|
||||
Assumption: the pinned Home Manager input contains the documented Lua Hyprland generator.
|
||||
Validate this during the configuration build.
|
||||
8
docs/03 wrk/dotfiles.md
Normal file
8
docs/03 wrk/dotfiles.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
aliases:
|
||||
- Dotfiles
|
||||
date-created: 2026-10-14 16:27
|
||||
date-modified: 2026-10-14 16:28
|
||||
---
|
||||
|
||||
# Dotfiles
|
||||
8
docs/03 wrk/productivity.md
Normal file
8
docs/03 wrk/productivity.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
aliases:
|
||||
- Productivity
|
||||
date-created: 2026-10-14 16:32
|
||||
date-modified: 2026-10-14 16:33
|
||||
---
|
||||
|
||||
# Productivity
|
||||
15
docs/03 wrk/tasks/0001-todo-cli.md
Normal file
15
docs/03 wrk/tasks/0001-todo-cli.md
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
aliases:
|
||||
- "0001"
|
||||
- TODO CLI
|
||||
date-created: 2026-10-14 00:49
|
||||
date-modified: 2026-10-14 16:33
|
||||
status: open
|
||||
area: "[[productivity]]"
|
||||
blocked-by: []
|
||||
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.
|
||||
103
docs/03 wrk/tasks/0002-nixos-refactor.md
Normal file
103
docs/03 wrk/tasks/0002-nixos-refactor.md
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
aliases:
|
||||
- "0002"
|
||||
- NixOS Refactor
|
||||
date-created: 2026-10-14 16:07
|
||||
date-modified: 2026-10-14 18:17
|
||||
status: open
|
||||
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`
|
||||
- [ ] `configs/nixos/modules/services/homepage.nix`
|
||||
- [ ] `configs/nixos/modules/services/navidrome.nix`
|
||||
- [ ] `configs/nixos/modules/services/reverse-proxy.nix`
|
||||
- [ ] `configs/nixos/modules/services/steamcmd.nix`
|
||||
- [ ] `configs/nixos/modules/services/games/palworld.nix`
|
||||
- [ ] `configs/nixos/modules/services/games/valheim.nix`
|
||||
- [ ] `configs/nixos/modules/shell/bazel.nix`
|
||||
- [ ] `configs/nixos/modules/shell/direnv/default.nix`
|
||||
- [ ] `configs/nixos/modules/shell/fish/default.nix`
|
||||
- [ ] `configs/nixos/modules/shell/gitea-axi.nix`
|
||||
- [ ] `configs/nixos/modules/shell/git.nix`
|
||||
- [ ] `configs/nixos/modules/shell/herdr.nix`
|
||||
- [ ] `configs/nixos/modules/shell/nvim/default.nix`
|
||||
- [ ] `configs/nixos/modules/shell/omp.nix`
|
||||
- [ ] `configs/nixos/modules/shell/pi/default.nix`
|
||||
- [ ] `configs/nixos/modules/shell/python.nix`
|
||||
- [ ] `configs/nixos/modules/shell/rust.nix`
|
||||
- [ ] `configs/nixos/modules/shell/tmux/default.nix`
|
||||
- [ ] `configs/nixos/modules/shell/yazi.nix`
|
||||
- [ ] `src/dotfiles/guests.nix`
|
||||
- [x] `configs/nixos/guests/actualbudget.nix`
|
||||
- [ ] `configs/nixos/guests/copyparty.nix`
|
||||
- [ ] `configs/nixos/guests/homepage.nix`
|
||||
- [ ] `configs/nixos/guests/navidrome.nix`
|
||||
- [ ] `configs/nixos/guests/palworld.nix`
|
||||
- [ ] `configs/nixos/guests/reverse-proxy.nix`
|
||||
- [ ] `configs/nixos/guests/valheim.nix`
|
||||
- [ ] `src/dotfiles/hosts.nix`
|
||||
- [ ] `configs/nixos/hosts/neogaia/default.nix`
|
||||
- [ ] `configs/nixos/hosts/neogaia/disk.nix`
|
||||
- [ ] `configs/nixos/hosts/neogaia/hardware-configuration.nix`
|
||||
- [ ] `configs/nixos/hosts/pikachu/default.nix`
|
||||
- [ ] `configs/nixos/hosts/pikachu/disk.nix`
|
||||
- [ ] `configs/nixos/hosts/pikachu/hardware-configuration.nix`
|
||||
- [ ] `configs/nixos/hosts/zeus/default.nix`
|
||||
- [ ] `configs/nixos/hosts/zeus/disk.nix`
|
||||
- [ ] `configs/nixos/hosts/zeus/hardware-configuration.nix`
|
||||
37
docs/03 wrk/tasks/0003-whisper-dictation.md
Normal file
37
docs/03 wrk/tasks/0003-whisper-dictation.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
aliases:
|
||||
- "0003"
|
||||
- Whisper dictation
|
||||
date-created: 2026-10-14 16:31
|
||||
date-modified: 2026-10-14 16:33
|
||||
status: open
|
||||
area: "[[productivity]]"
|
||||
blocked-by:
|
||||
- "[[0002-nixos-refactor]]"
|
||||
subtasks: []
|
||||
---
|
||||
|
||||
# Whisper dictation
|
||||
|
||||
Provide reliable local dictation on Zeus.
|
||||
|
||||
## Prototype findings
|
||||
|
||||
- `hyprwhspr-rs` provides PipeWire capture, local `whisper.cpp` transcription, clipboard copying, and Hyprland integration.
|
||||
- The prototype used `small.en` from `~/dwn/ggml-small.en.bin`.
|
||||
- The initial CPU prototype transcribed accurately but added about 2.3 seconds after a 2.9-second recording.
|
||||
- Vulkan acceleration uses `whisper-cpp-vulkan` and `gpu_layers = 999`.
|
||||
- `SUPER + ALT + D` starts recording on press and stops recording on release.
|
||||
- Hyprland Lua represents release bindings with `hl.bind(..., { release = true })`.
|
||||
- `hyprwhspr-rs` uses Hyprland's removed `sendshortcut` dispatcher for automatic paste.
|
||||
- Its virtual-keyboard fallback can drop modifiers and insert only `v`.
|
||||
- Treat the prototype configuration as disposable because it no longer works reliably.
|
||||
|
||||
## Completion
|
||||
|
||||
- [ ] Choose a maintained local speech-to-text backend.
|
||||
- [ ] Record microphone audio through PipeWire.
|
||||
- [ ] Transcribe locally with GPU acceleration when available.
|
||||
- [ ] Insert transcribed text reliably into the focused application.
|
||||
- [ ] Copy transcribed text to the clipboard.
|
||||
- [ ] Test dictation with representative technical speech.
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
date-created: {{date}} {{time}}
|
||||
date-created: "{{date}} {{time}}"
|
||||
---
|
||||
|
||||
# {{title}}
|
||||
|
||||
11
docs/99 meta/templates/task.md
Normal file
11
docs/99 meta/templates/task.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
date-created: "{{date}} {{time}}"
|
||||
status: open # Allowed: open, closed
|
||||
area:
|
||||
blocked-by:
|
||||
-
|
||||
subtasks:
|
||||
-
|
||||
---
|
||||
|
||||
# {{title}}
|
||||
168
docs/TODO.md
168
docs/TODO.md
@@ -1,23 +1,25 @@
|
||||
# active projects
|
||||
# Active Projects
|
||||
|
||||
- dotfiles
|
||||
- nix-style / refactor
|
||||
|
||||
## Inactive For Now
|
||||
|
||||
- ariadne
|
||||
- machoke
|
||||
- falinks
|
||||
- dotfiles
|
||||
- natu
|
||||
- nix-style / refactor
|
||||
|
||||
# project ideas
|
||||
# Project Ideas
|
||||
|
||||
- onepacerr (requires raichu migration)
|
||||
- obsidian-source-notes (`ingest-*` skills)
|
||||
- obsidian-game-backlog
|
||||
- whisper dictation
|
||||
- breadcrumbs
|
||||
- gingko
|
||||
- pokemon-classifier-ml
|
||||
- home assistant agent integration
|
||||
- wallpaper randomizer
|
||||
- ebook tool (might be an overlap with `ingest-ebook`)
|
||||
- amazon / actualbudget exporter
|
||||
- vim-obsidian-markdown
|
||||
- dictionary cli tool
|
||||
- kitchen.md
|
||||
@@ -25,10 +27,11 @@
|
||||
- random number generator utility
|
||||
- reading material helper
|
||||
- valheim progression DSL + graph
|
||||
- Pi:
|
||||
- vim bindings
|
||||
|
||||
# migration
|
||||
# Migration
|
||||
|
||||
- [[conventional-commits]] -> skill?
|
||||
- [[dictionary]] -> cli tool
|
||||
- [[technical-writing]] -> skill?
|
||||
- gitea-axi
|
||||
@@ -50,7 +53,7 @@
|
||||
- subagents
|
||||
- test-driven-development
|
||||
|
||||
## pikachu + pichu proxmox -> nixos
|
||||
## Pikachu + Pichu Proxmox -> Nixos
|
||||
|
||||
- lidarr (requires raichu)
|
||||
- immich
|
||||
@@ -60,7 +63,8 @@
|
||||
- adguard
|
||||
- manage DNS rewrites for local service names such as `home`
|
||||
|
||||
## raichu proxmox -> nixos
|
||||
## Raichu Proxmox -> Nixos
|
||||
|
||||
- dwn torrent (vpn)
|
||||
- dwn usenet (vpn)
|
||||
- dwn-torrent-novpn
|
||||
@@ -77,39 +81,34 @@
|
||||
- seerr
|
||||
- plex
|
||||
- pterodactyl
|
||||
- palworld old
|
||||
- project zomboid
|
||||
- minecraft
|
||||
- cobblemon
|
||||
- palworld old
|
||||
- project zomboid
|
||||
- minecraft
|
||||
- cobblemon
|
||||
|
||||
# Learning
|
||||
|
||||
# learning
|
||||
- YouTube
|
||||
- EeroAlvar
|
||||
- https://www.youtube.com/watch?v=DzhLFiblPUk
|
||||
- https://www.youtube.com/watch?v=hQGAwD9vkLo
|
||||
- https://www.youtube.com/watch?v=hhtExS5UQBI
|
||||
- https://www.youtube.com/watch?v=Y1xO9OsCBC4
|
||||
- https://www.youtube.com/watch?v=DWWrLlM3gwQ
|
||||
- https://www.youtube.com/watch?v=yHL-gmkzC-4
|
||||
- https://www.youtube.com/watch?v=KRVYUkM16hE
|
||||
- https://www.youtube.com/watch?v=RxdvzenLcl4
|
||||
- https://www.youtube.com/watch?v=mdbpIpPSPwQ
|
||||
- https://www.youtube.com/watch?v=GHsq0klC_4g
|
||||
- https://www.youtube.com/watch?v=ciC6ffUqI8k
|
||||
- https://www.youtube.com/watch?v=gYxZt9Qe0fk
|
||||
- https://www.youtube.com/watch?v=0tqty8ltKDA
|
||||
- https://www.youtube.com/watch?v=kzcI5F4tGiU
|
||||
- https://www.youtube.com/watch?v=iKwPaB5TUdI
|
||||
- https://www.youtube.com/watch?v=A-CtdQFRX64
|
||||
- natu literature
|
||||
- https://rust-analyzer.github.io/book/contributing/syntax.html?utm_source=chatgpt.com
|
||||
- https://github.com/dotnet/roslyn/blob/main/docs/compilers/Design/Red-Green%20Trees.md?utm_source=chatgpt.com
|
||||
- https://github.com/rust-analyzer/rowan/blob/master/examples/s_expressions.rs?utm_source=chatgpt.com
|
||||
- https://dl.acm.org/doi/10.1145/964001.964011
|
||||
- https://web.cecs.pdx.edu/~apt/esop15.pdf?utm_source=chatgpt.com
|
||||
- https://www.researchgate.net/publication/2377179_Efficient_and_flexible_incremental_parsing?utm_source=chatgpt.com
|
||||
- EeroAlvar
|
||||
- <https://www.youtube.com/watch?v=DzhLFiblPUk>
|
||||
- <https://www.youtube.com/watch?v=hQGAwD9vkLo>
|
||||
- <https://www.youtube.com/watch?v=hhtExS5UQBI>
|
||||
- <https://www.youtube.com/watch?v=Y1xO9OsCBC4>
|
||||
- <https://www.youtube.com/watch?v=DWWrLlM3gwQ>
|
||||
- <https://www.youtube.com/watch?v=yHL-gmkzC-4>
|
||||
- <https://www.youtube.com/watch?v=KRVYUkM16hE>
|
||||
- <https://www.youtube.com/watch?v=RxdvzenLcl4>
|
||||
- <https://www.youtube.com/watch?v=mdbpIpPSPwQ>
|
||||
- <https://www.youtube.com/watch?v=GHsq0klC_4g>
|
||||
- <https://www.youtube.com/watch?v=ciC6ffUqI8k>
|
||||
- <https://www.youtube.com/watch?v=gYxZt9Qe0fk>
|
||||
- <https://www.youtube.com/watch?v=0tqty8ltKDA>
|
||||
- <https://www.youtube.com/watch?v=kzcI5F4tGiU>
|
||||
- <https://www.youtube.com/watch?v=iKwPaB5TUdI>
|
||||
- <https://www.youtube.com/watch?v=A-CtdQFRX64>
|
||||
- <https://www.youtube.com/watch?v=MHPRnM38Dyc>
|
||||
|
||||
# tech debt
|
||||
# Tech Debt
|
||||
|
||||
- better remote deployment
|
||||
- expose remote deployment as a flake app, replacing the Fish-only `deploy-to` workflow
|
||||
@@ -118,92 +117,7 @@
|
||||
Prefer clear per-file dependency edges, but `rust_library` maps each target to a crate.
|
||||
Decide whether wrapper rules or another pattern can improve graph clarity without forcing artificial crate boundaries.
|
||||
|
||||
## nix refactor
|
||||
## Nix Refactor
|
||||
|
||||
Review Nix files in entrypoint order:
|
||||
|
||||
- [x] `flake.nix`
|
||||
- [x] `src/flake/default.nix`
|
||||
- [x] `src/flake/formatter.nix`
|
||||
- [x] `src/flake/apps.nix`
|
||||
- [x] `src/dotfiles/default.nix`
|
||||
- [x] `src/dotfiles/util.nix`
|
||||
- [x] `src/dotfiles/registry.nix`
|
||||
- [x] `src/dotfiles/registry/schema.nix`
|
||||
- [x] `src/dotfiles/registry/validation.nix`
|
||||
- [x] `configs/nixos/registry/address.nix`
|
||||
- [>] `configs/nixos/registry/homepage.nix`
|
||||
- [x] `configs/nixos/registry/route.nix`
|
||||
- [x] `configs/nixos/registry/shell-shortcut.nix`
|
||||
- [x] `configs/nixos/registry/stable-uid.nix`
|
||||
- [x] `configs/nixos/modules/default.nix`
|
||||
- [x] `configs/nixos/modules/host.nix`
|
||||
- [x] `configs/nixos/modules/guest.nix`
|
||||
- [x] `configs/nixos/modules/monorepo.nix`
|
||||
- [x] `configs/nixos/modules/zfs.nix`
|
||||
- [x] `configs/nixos/modules/network/ssh.nix`
|
||||
- [x] `configs/nixos/modules/network/vlans.nix`
|
||||
- [x] `configs/nixos/modules/network/vpn-client.nix`
|
||||
- [x] `configs/nixos/modules/desktop/default.nix`
|
||||
- [x] `configs/nixos/modules/desktop/apps/alacritty.nix`
|
||||
- [x] `configs/nixos/modules/desktop/apps/firefox.nix`
|
||||
- [x] `configs/nixos/modules/desktop/apps/ghostty.nix`
|
||||
- [x] `configs/nixos/modules/desktop/apps/obsidian.nix`
|
||||
- [x] `configs/nixos/modules/desktop/apps/r2modman.nix`
|
||||
- [x] `configs/nixos/modules/desktop/apps/signal.nix`
|
||||
- [x] `configs/nixos/modules/desktop/apps/steam.nix`
|
||||
- [x] `configs/nixos/modules/desktop/apps/vencord.nix`
|
||||
- [ ] `configs/nixos/modules/desktop/session/hyprland/default.nix`
|
||||
- [ ] `configs/nixos/modules/desktop/session/hyprland/hypridle.nix`
|
||||
- [ ] `configs/nixos/modules/desktop/session/hyprland/hyprlock.nix`
|
||||
- [ ] `configs/nixos/modules/desktop/session/login.nix`
|
||||
- [ ] `configs/nixos/modules/desktop/session/mako.nix`
|
||||
- [ ] `configs/nixos/modules/desktop/session/portals.nix`
|
||||
- [ ] `configs/nixos/modules/desktop/session/rofi.nix`
|
||||
- [ ] `configs/nixos/modules/desktop/session/waybar.nix`
|
||||
- [ ] `configs/nixos/modules/desktop/utilities/audio.nix`
|
||||
- [ ] `configs/nixos/modules/desktop/utilities/clipboard.nix`
|
||||
- [ ] `configs/nixos/modules/desktop/utilities/osd.nix`
|
||||
- [ ] `configs/nixos/modules/desktop/utilities/recording.nix`
|
||||
- [ ] `configs/nixos/modules/desktop/utilities/screenshot.nix`
|
||||
- [ ] `configs/nixos/modules/desktop/utilities/theming.nix`
|
||||
- [ ] `configs/nixos/modules/desktop/utilities/userdirs.nix`
|
||||
- [ ] `configs/nixos/modules/services/actualbudget.nix`
|
||||
- [ ] `configs/nixos/modules/services/copyparty.nix`
|
||||
- [ ] `configs/nixos/modules/services/homepage.nix`
|
||||
- [ ] `configs/nixos/modules/services/navidrome.nix`
|
||||
- [ ] `configs/nixos/modules/services/reverse-proxy.nix`
|
||||
- [ ] `configs/nixos/modules/services/steamcmd.nix`
|
||||
- [ ] `configs/nixos/modules/services/games/palworld.nix`
|
||||
- [ ] `configs/nixos/modules/services/games/valheim.nix`
|
||||
- [ ] `configs/nixos/modules/shell/bazel.nix`
|
||||
- [ ] `configs/nixos/modules/shell/direnv/default.nix`
|
||||
- [ ] `configs/nixos/modules/shell/fish/default.nix`
|
||||
- [ ] `configs/nixos/modules/shell/gitea-axi.nix`
|
||||
- [ ] `configs/nixos/modules/shell/git.nix`
|
||||
- [ ] `configs/nixos/modules/shell/herdr.nix`
|
||||
- [ ] `configs/nixos/modules/shell/nvim/default.nix`
|
||||
- [ ] `configs/nixos/modules/shell/omp.nix`
|
||||
- [ ] `configs/nixos/modules/shell/pi/default.nix`
|
||||
- [ ] `configs/nixos/modules/shell/python.nix`
|
||||
- [ ] `configs/nixos/modules/shell/rust.nix`
|
||||
- [ ] `configs/nixos/modules/shell/tmux/default.nix`
|
||||
- [ ] `configs/nixos/modules/shell/yazi.nix`
|
||||
- [ ] `src/dotfiles/guests.nix`
|
||||
- [ ] `configs/nixos/guests/actualbudget.nix`
|
||||
- [ ] `configs/nixos/guests/copyparty.nix`
|
||||
- [ ] `configs/nixos/guests/homepage.nix`
|
||||
- [ ] `configs/nixos/guests/navidrome.nix`
|
||||
- [ ] `configs/nixos/guests/palworld.nix`
|
||||
- [ ] `configs/nixos/guests/reverse-proxy.nix`
|
||||
- [ ] `configs/nixos/guests/valheim.nix`
|
||||
- [ ] `src/dotfiles/hosts.nix`
|
||||
- [ ] `configs/nixos/hosts/neogaia/default.nix`
|
||||
- [ ] `configs/nixos/hosts/neogaia/disk.nix`
|
||||
- [ ] `configs/nixos/hosts/neogaia/hardware-configuration.nix`
|
||||
- [ ] `configs/nixos/hosts/pikachu/default.nix`
|
||||
- [ ] `configs/nixos/hosts/pikachu/disk.nix`
|
||||
- [ ] `configs/nixos/hosts/pikachu/hardware-configuration.nix`
|
||||
- [ ] `configs/nixos/hosts/zeus/default.nix`
|
||||
- [ ] `configs/nixos/hosts/zeus/disk.nix`
|
||||
- [ ] `configs/nixos/hosts/zeus/hardware-configuration.nix`
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
# Conventional Commits 1.0.0
|
||||
|
||||
## Summary
|
||||
|
||||
The Conventional Commits specification is a lightweight convention on top of commit messages.
|
||||
It provides an easy set of rules for creating an explicit commit history;
|
||||
which makes it easier to write automated tools on top of.
|
||||
This convention dovetails with [SemVer](http://semver.org),
|
||||
by describing the features, fixes, and breaking changes made in commit messages.
|
||||
|
||||
The commit message should be structured as follows:
|
||||
|
||||
---
|
||||
|
||||
```
|
||||
<type>[optional scope]: <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer(s)]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
The commit contains the following structural elements, to communicate intent to the
|
||||
consumers of your library:
|
||||
|
||||
1. **fix:** a commit of the _type_ `fix` patches a bug in your codebase (this correlates with [`PATCH`](http://semver.org/#summary) in Semantic Versioning).
|
||||
2. **feat:** a commit of the _type_ `feat` introduces a new feature to the codebase (this correlates with [`MINOR`](http://semver.org/#summary) in Semantic Versioning).
|
||||
3. **BREAKING CHANGE:** a commit that has a footer `BREAKING CHANGE:`, or appends a `!` after the type/scope, introduces a breaking API change (correlating with [`MAJOR`](http://semver.org/#summary) in Semantic Versioning).
|
||||
A BREAKING CHANGE can be part of commits of any _type_.
|
||||
4. _types_ other than `fix:` and `feat:` are allowed, for example [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional) (based on the [Angular convention](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#type)) recommends `build:`, `chore:`,
|
||||
`ci:`, `docs:`, `style:`, `refactor:`, `perf:`, `test:`, and others.
|
||||
5. _footers_ other than `BREAKING CHANGE: <description>` may be provided and follow a convention similar to
|
||||
[git trailer format](https://git-scm.com/docs/git-interpret-trailers).
|
||||
|
||||
Additional types are not mandated by the Conventional Commits specification, and have no implicit effect in Semantic Versioning (unless they include a BREAKING CHANGE).
|
||||
A scope may be provided to a commit’s type, to provide additional contextual information and is contained within parenthesis, e.g., `feat(parser): add ability to parse arrays`.
|
||||
|
||||
## Examples
|
||||
|
||||
### Commit message with description and breaking change footer
|
||||
|
||||
```
|
||||
feat: allow provided config object to extend other configs
|
||||
|
||||
BREAKING CHANGE: `extends` key in config file is now used for extending other config files
|
||||
```
|
||||
|
||||
### Commit message with `!` to draw attention to breaking change
|
||||
|
||||
```
|
||||
feat!: send an email to the customer when a product is shipped
|
||||
```
|
||||
|
||||
### Commit message with scope and `!` to draw attention to breaking change
|
||||
|
||||
```
|
||||
feat(api)!: send an email to the customer when a product is shipped
|
||||
```
|
||||
|
||||
### Commit message with both `!` and BREAKING CHANGE footer
|
||||
|
||||
```
|
||||
chore!: drop support for Node 6
|
||||
|
||||
BREAKING CHANGE: use JavaScript features not available in Node 6.
|
||||
```
|
||||
|
||||
### Commit message with no body
|
||||
|
||||
```
|
||||
docs: correct spelling of CHANGELOG
|
||||
```
|
||||
|
||||
### Commit message with scope
|
||||
|
||||
```
|
||||
feat(lang): add Polish language
|
||||
```
|
||||
|
||||
### Commit message with multi-paragraph body and multiple footers
|
||||
|
||||
```
|
||||
fix: prevent racing of requests
|
||||
|
||||
Introduce a request id and a reference to latest request. Dismiss
|
||||
incoming responses other than from latest request.
|
||||
|
||||
Remove timeouts which were used to mitigate the racing issue but are
|
||||
obsolete now.
|
||||
|
||||
Reviewed-by: Z
|
||||
Refs: #123
|
||||
```
|
||||
|
||||
## Specification
|
||||
|
||||
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt).
|
||||
|
||||
1. Commits MUST be prefixed with a type, which consists of a noun, `feat`, `fix`, etc.,
|
||||
followed by the OPTIONAL scope, OPTIONAL `!`, and REQUIRED terminal colon and space.
|
||||
2. The type `feat` MUST be used when a commit adds a new feature to your application or library.
|
||||
3. The type `fix` MUST be used when a commit represents a bug fix for your application.
|
||||
4. A scope MAY be provided after a type.
|
||||
A scope MUST consist of a noun describing a section of the codebase surrounded by parenthesis, e.g., `fix(parser):`
|
||||
5. A description MUST immediately follow the colon and space after the type/scope prefix.
|
||||
The description is a short summary of the code changes, e.g., _fix: array parsing issue when multiple spaces were contained in string_.
|
||||
6. A longer commit body MAY be provided after the short description, providing additional contextual information about the code changes.
|
||||
The body MUST begin one blank line after the description.
|
||||
7. A commit body is free-form and MAY consist of any number of newline separated paragraphs.
|
||||
8. One or more footers MAY be provided one blank line after the body.
|
||||
Each footer MUST consist of a word token, followed by either a `:<space>` or `<space>#` separator, followed by a string value (this is inspired by the [git trailer convention](https://git-scm.com/docs/git-interpret-trailers)).
|
||||
9. A footer’s token MUST use `-` in place of whitespace characters, e.g., `Acked-by` (this helps differentiate the footer section from a multi-paragraph body).
|
||||
An exception is made for `BREAKING CHANGE`, which MAY also be used as a token.
|
||||
10. A footer’s value MAY contain spaces and newlines, and parsing MUST terminate when the next valid footer token/separator pair is observed.
|
||||
11. Breaking changes MUST be indicated in the type/scope prefix of a commit, or as an entry in the footer.
|
||||
12. If included as a footer, a breaking change MUST consist of the uppercase text BREAKING CHANGE, followed by a colon, space, and description, e.g., _BREAKING CHANGE: environment variables now take precedence over config files_.
|
||||
13. If included in the type/scope prefix, breaking changes MUST be indicated by a `!` immediately before the `:`.
|
||||
If `!` is used, `BREAKING CHANGE:` MAY be omitted from the footer section, and the commit description SHALL be used to describe the breaking change.
|
||||
14. Types other than `feat` and `fix` MAY be used in your commit messages, e.g., _docs: updated ref docs._
|
||||
15. The units of information that make up Conventional Commits MUST NOT be treated as case sensitive by implementors, with the exception of BREAKING CHANGE which MUST be uppercase.
|
||||
16. BREAKING-CHANGE MUST be synonymous with BREAKING CHANGE, when used as a token in a footer.
|
||||
|
||||
## FAQ
|
||||
|
||||
### How does Conventional Commits handle revert commits?
|
||||
|
||||
Reverting code can be complicated: are you reverting multiple commits?
|
||||
if you are reverting a feature, should the next release instead be a patch?
|
||||
|
||||
Conventional Commits does not make an explicit effort to define revert behavior.
|
||||
Instead we leave it to tooling authors to use the flexibility of _types_ and _footers_ to develop their logic for handling reverts.
|
||||
|
||||
One recommendation is to use the `revert` type, and a footer that references the commit SHAs that are being reverted:
|
||||
|
||||
```
|
||||
revert: let us never again speak of the noodle incident
|
||||
|
||||
Refs: 676104e, a215868
|
||||
```
|
||||
@@ -38,7 +38,7 @@ Use `REPLACE` for terms that have a preferred substitute.
|
||||
| host | USE | | A machine with a NixOS configuration built by this repository's flake. |
|
||||
| module | USE | | A self-contained NixOS configuration under `configs/nixos/modules/`. |
|
||||
| registry | USE | | A typed configuration namespace whose entries can be collected across hosts and reused by other configuration. |
|
||||
| Conventional Commits | USE | | A commit-message format defined in `docs/conventional-commits.md`. |
|
||||
| Conventional Commits | USE | | A format for commit messages. |
|
||||
| forge | USE | | A web service that hosts Git repositories, pull requests, issues, and code review comments. Use when the exact provider does not matter. |
|
||||
| flake | USE | | A Nix project with pinned inputs and declared outputs. Use for the repository-level build unit defined by `flake.nix`. |
|
||||
| NixOS module | USE | | A Nix file evaluated by `nixosSystem` for a NixOS configuration. |
|
||||
|
||||
107
flake.lock
generated
107
flake.lock
generated
@@ -75,11 +75,11 @@
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1789232619,
|
||||
"narHash": "sha256-8pyRIm67V5dtVN3A4Mu0e2a7DIhUhWp6y3NSVS6WBwU=",
|
||||
"lastModified": 1792048969,
|
||||
"narHash": "sha256-+Scy7etDouWzvEV9d6zEDz/TxOoGzfSJau9+yp/zWM4=",
|
||||
"owner": "chaotic-cx",
|
||||
"repo": "nyx",
|
||||
"rev": "0265d6932168aef12c47f03a565786410ba877e0",
|
||||
"rev": "c283697bfcc3770232139b86c4ad19fd8a62cd6f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -96,11 +96,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1781152676,
|
||||
"narHash": "sha256-RxWs5ND31KzTG7wvMM+PMfUjyNpmIEr999lqNARaM5o=",
|
||||
"lastModified": 1789770686,
|
||||
"narHash": "sha256-uZkBR7yHdIKUFB5SZdfgh1qkGfI3XmYmI/lTiquxbck=",
|
||||
"owner": "nix-community",
|
||||
"repo": "disko",
|
||||
"rev": "ff8702b4de27f72b4c78573dfb89ec74e36abdf1",
|
||||
"rev": "725ea35e410ad83be4931d1bff7e090eacaf3563",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -117,11 +117,11 @@
|
||||
},
|
||||
"locked": {
|
||||
"dir": "pkgs/firefox-addons",
|
||||
"lastModified": 1789185767,
|
||||
"narHash": "sha256-jaWL9beN3ifjNzNEBxo0abA+TSi0n7Q8LRQvZTxEqaM=",
|
||||
"lastModified": 1792036980,
|
||||
"narHash": "sha256-y28wLbzsIbBDeToXHhAdoTs+lcH/d7/1Pky40N6/CoI=",
|
||||
"owner": "rycee",
|
||||
"repo": "nur-expressions",
|
||||
"rev": "811170bb74b11f3b9489618ce77487cc58c79067",
|
||||
"rev": "cbf0380669792dd8f6dbb891d3be33aae8fa4625",
|
||||
"type": "gitlab"
|
||||
},
|
||||
"original": {
|
||||
@@ -155,11 +155,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1788450739,
|
||||
"narHash": "sha256-glZLQlzIn1fXH6PazR2iUmTo7kzzyYSshrWhLS9TqCU=",
|
||||
"lastModified": 1790898470,
|
||||
"narHash": "sha256-zTwuwezD0w3hpga6a6P8emidzAZFyWqlNeRBlpWOOl8=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "31729ca8cbdb4fa927b34e5f4353e6a83f39e993",
|
||||
"rev": "024633cd702b10285db5cb19b40ad48d2399ba60",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -267,11 +267,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1789183968,
|
||||
"narHash": "sha256-pEWnYdIF1pBMZwarp/rEPzl+Lknhm5hhjSfeLxH8nAA=",
|
||||
"lastModified": 1791940130,
|
||||
"narHash": "sha256-hRCCi4fp5IjLz4cA8+RkUsaYAHzbd7M5FSAjKOq4E+k=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "cd1c9e552f41894aeb5cc5cb353d5a1d61550357",
|
||||
"rev": "92a0ed1be12f1e20093faf8af63bae2c74ad521e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -308,11 +308,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1789267719,
|
||||
"narHash": "sha256-FIlwGdFjtpGYyL8g/RZZvIxshT9gdwJfcMjHRWHrc4U=",
|
||||
"lastModified": 1792098797,
|
||||
"narHash": "sha256-u4DIIZgznEGzByxXrxWCXK19VcMtTBmi/M9gZ7Jk9bM=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "10d0acd7f81e3c035b452daa39ac7d1673880729",
|
||||
"rev": "640725978b99076e18e069f6e7d2959124584f8f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -328,11 +328,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1789127271,
|
||||
"narHash": "sha256-vL/Fpo7Zxa7wvO8V5FAToUBvP+K3XsMB1wUglTtd/J0=",
|
||||
"lastModified": 1791721963,
|
||||
"narHash": "sha256-BYhx89XJltnRyQeuDKHVL5eSqbbfAcOKuDts7QRsrxQ=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixos-hardware",
|
||||
"rev": "24cfdc1f9344b90a1eee329a3906e2f39f4d0f1e",
|
||||
"rev": "60a624466c08787ee8303393b7d4752d1826f14f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -343,27 +343,24 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1789006805,
|
||||
"narHash": "sha256-xB8mKMOx1IA9vTDNLmJZ6n4wCMq/cuWBBOzGCRnqxrU=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "8ce4ef6cb6f871616146b9fe26d2a5ae594e94fe",
|
||||
"type": "github"
|
||||
"lastModified": 1791734076,
|
||||
"narHash": "sha256-0dNcyVeoD61zhucIpt5BAQ+ytd6JzF641Z+A9bgltLo=",
|
||||
"rev": "7fd4004d0511991fff3a0524856a9be09e28c5f0",
|
||||
"type": "tarball",
|
||||
"url": "https://releases.nixos.org/nixos/unstable/nixos-26.11pre1090327.7fd4004d0511/nixexprs.tar.zst"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
"type": "tarball",
|
||||
"url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.zst"
|
||||
}
|
||||
},
|
||||
"nixpkgs-stable": {
|
||||
"locked": {
|
||||
"lastModified": 1789114715,
|
||||
"narHash": "sha256-ugpsyk3NM2s87vXfUiIIiibbJ4Pp0JPS5p/3mfs+q+c=",
|
||||
"lastModified": 1791883866,
|
||||
"narHash": "sha256-yvkDdToRZ4IgPc9tSBzzCYJvTLf8n5eE3rgUkpCuD5A=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "21a67dc470149f337cecafbe965d8d252a390518",
|
||||
"rev": "a033e66ab94a6bedf2c3d58d93a36319d3cbbbaf",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -375,11 +372,11 @@
|
||||
},
|
||||
"nixpkgs-unstable": {
|
||||
"locked": {
|
||||
"lastModified": 1789073787,
|
||||
"narHash": "sha256-xfX/toC2QV707s06GbP4II/TxYF0fNQj7s5/LClNDKc=",
|
||||
"lastModified": 1791966044,
|
||||
"narHash": "sha256-t1hT8UW97rFCWCJO7/pClvu0AhLN5V3uir2M0ic4w/Q=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "aff8a0b28396750446e5537a96461bc4facdb287",
|
||||
"rev": "319e4cebef10cbb636c6ea392137b52ad105336a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -391,11 +388,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1789149629,
|
||||
"narHash": "sha256-H6GwaZzZf+4npqv0tph94w9tZddSjFjmQrVsW0z78uk=",
|
||||
"lastModified": 1791956154,
|
||||
"narHash": "sha256-jBYGPi4udQ9uo4KQp9m2J0546nJVQitG15gvZSaVVAs=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "eaad089433ca2bb662274377d33df3d0e51ef28b",
|
||||
"rev": "55bfb4d7437bd4e09ab7cba58a43bd68eb310044",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -414,11 +411,11 @@
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1789067777,
|
||||
"narHash": "sha256-gN3OnYgJwCK0HX01dQGWusnirJuVHE9WigmSqOnyuCE=",
|
||||
"lastModified": 1792012326,
|
||||
"narHash": "sha256-DApZIjaKdU4XzglZ56yrPYLhG/ajoyuQ4g4s8tkHqxU=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixvim",
|
||||
"rev": "16baff93297c63b6d4c497f00ffcf0b844b06fc6",
|
||||
"rev": "8485794923eb406ce3728af320392226608f724b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -476,11 +473,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1788914643,
|
||||
"narHash": "sha256-4GuMPW90JSxXWDPUB9M+1m7fYbe3H0apOd86/zBQ2Kw=",
|
||||
"lastModified": 1792056300,
|
||||
"narHash": "sha256-YU1LjDe1o2Ymg5zEiCHbmMXLkDCtxHQRiVqZ3nC5GGk=",
|
||||
"owner": "Mic92",
|
||||
"repo": "sops-nix",
|
||||
"rev": "13616fff713a9f94055c66f15687ebdc17a335df",
|
||||
"rev": "44931708c62937c2b1bf7953902989fa5bb9ee1b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -509,11 +506,11 @@
|
||||
"tinted-zed": "tinted-zed"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1787771653,
|
||||
"narHash": "sha256-DkkJSBOXWV/mja5Vy8az5g1KpZlsbUwlmH0BsQhowaQ=",
|
||||
"lastModified": 1791574276,
|
||||
"narHash": "sha256-UMnLnZ3JlA7UTBNNNx1rNsXYoPP/cirXLCKGiQ/yDx8=",
|
||||
"owner": "danth",
|
||||
"repo": "stylix",
|
||||
"rev": "5e3809851f486e7fc7e84b40f174c74b60ecc784",
|
||||
"rev": "2856c4c6a845b9bec98828ef197abe176e4806bb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -525,11 +522,11 @@
|
||||
"systems": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1774449309,
|
||||
"narHash": "sha256-brhZ8DmuGtzkCYHJg4HEd602amKm89Y9ytsFZ5uWD1w=",
|
||||
"lastModified": 1791244623,
|
||||
"narHash": "sha256-uuanbkFk8a313Zfrxdcz8Us/yQtaxNnKJtX8v0ZPpd8=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "c29398b59d2048c4ab79345812849c9bd15e9150",
|
||||
"rev": "d36eeed142f88b5a636f2e3856a53c61654cf29b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -626,11 +623,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1786901030,
|
||||
"narHash": "sha256-WSFCsDSE5ffgD2MqzkM2CYjeFiKhRF/dJUN8uedb6YE=",
|
||||
"lastModified": 1791885023,
|
||||
"narHash": "sha256-C3alQ2MEt0zVrUSho5NUDo3jmhbff72++bfwoqT9Omo=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "27b3b12a8e6375f28ebe122f07d230ca5459bbfa",
|
||||
"rev": "0d6d739a833ba9a9c93034862e4a7b061a062a1c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -14,7 +14,7 @@ let
|
||||
registry = registry.collected;
|
||||
};
|
||||
|
||||
util = import ./util.nix { inherit lib; };
|
||||
util = import ./util { inherit lib; };
|
||||
|
||||
registry = import ./registry.nix {
|
||||
inherit lib util;
|
||||
@@ -33,7 +33,12 @@ let
|
||||
repositoryModules = modules;
|
||||
root = root + "/guests";
|
||||
};
|
||||
modules.imports = util.collectNixFiles (root + "/modules");
|
||||
modules.imports = [
|
||||
{
|
||||
nixpkgs.overlays = [ self.overlays.custom ];
|
||||
}
|
||||
]
|
||||
++ util.collectNixFiles (root + "/modules");
|
||||
|
||||
in
|
||||
import ./hosts.nix {
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
{ lib }:
|
||||
let
|
||||
collectNixFiles =
|
||||
dir:
|
||||
lib.flatten (
|
||||
lib.mapAttrsToList (
|
||||
name: type:
|
||||
let
|
||||
path = dir + "/${name}";
|
||||
in
|
||||
if type == "directory" then
|
||||
collectNixFiles path
|
||||
else if type == "regular" && lib.hasSuffix ".nix" name then
|
||||
[ path ]
|
||||
else
|
||||
[ ]
|
||||
) (builtins.readDir dir)
|
||||
);
|
||||
in
|
||||
{
|
||||
inherit collectNixFiles;
|
||||
|
||||
removeNixSuffix = path: lib.removeSuffix ".nix" (builtins.baseNameOf path);
|
||||
|
||||
registry.predicates.uniqueValue =
|
||||
value: entries: builtins.length (lib.unique (map value entries)) == builtins.length entries;
|
||||
|
||||
bridgeName = id: "br-vlan${toString id}";
|
||||
|
||||
deriveMac =
|
||||
name:
|
||||
let
|
||||
hash = builtins.hashString "sha256" name;
|
||||
in
|
||||
lib.concatStringsSep ":" (
|
||||
[ "02" ]
|
||||
++ map (i: builtins.substring (i * 2) 2 hash) [
|
||||
0
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
]
|
||||
);
|
||||
}
|
||||
63
src/dotfiles/util/default.nix
Normal file
63
src/dotfiles/util/default.nix
Normal file
@@ -0,0 +1,63 @@
|
||||
{ lib }:
|
||||
let
|
||||
collectNixFiles =
|
||||
dir:
|
||||
lib.flatten (
|
||||
lib.mapAttrsToList (
|
||||
name: type:
|
||||
let
|
||||
path = dir + "/${name}";
|
||||
in
|
||||
if type == "directory" then
|
||||
collectNixFiles path
|
||||
else if type == "regular" && lib.hasSuffix ".nix" name then
|
||||
[ path ]
|
||||
else
|
||||
[ ]
|
||||
) (builtins.readDir dir)
|
||||
);
|
||||
|
||||
uniqueValue =
|
||||
value: entries: builtins.length (lib.unique (map value entries)) == builtins.length entries;
|
||||
in
|
||||
{
|
||||
formats.copyparty = import ./format-copyparty.nix { inherit lib; };
|
||||
|
||||
inherit collectNixFiles;
|
||||
removeNixSuffix = path: lib.removeSuffix ".nix" (builtins.baseNameOf path);
|
||||
|
||||
glyph =
|
||||
code:
|
||||
builtins.fromJSON (
|
||||
"\""
|
||||
+ (if builtins.isList code then lib.concatMapStrings (part: "\\u${part}") code else "\\u${code}")
|
||||
+ "\""
|
||||
);
|
||||
|
||||
registry.predicates.uniqueValue = uniqueValue;
|
||||
|
||||
registry.predicates.uniquePerHost =
|
||||
value:
|
||||
uniqueValue (entry: {
|
||||
inherit (entry) host;
|
||||
value = value entry;
|
||||
});
|
||||
|
||||
bridgeName = id: "br-vlan${toString id}";
|
||||
|
||||
deriveMac =
|
||||
name:
|
||||
let
|
||||
hash = builtins.hashString "sha256" name;
|
||||
in
|
||||
lib.concatStringsSep ":" (
|
||||
[ "02" ]
|
||||
++ map (i: builtins.substring (i * 2) 2 hash) [
|
||||
0
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
]
|
||||
);
|
||||
}
|
||||
58
src/dotfiles/util/format-copyparty.nix
Normal file
58
src/dotfiles/util/format-copyparty.nix
Normal file
@@ -0,0 +1,58 @@
|
||||
{ lib }:
|
||||
let
|
||||
renderLine =
|
||||
indent: name: value:
|
||||
if value == true then
|
||||
"${indent}${name}"
|
||||
else if value == false then
|
||||
null
|
||||
else if lib.isList value then
|
||||
"${indent}${name}: ${lib.concatStringsSep ", " (map toString value)}"
|
||||
else
|
||||
"${indent}${name}: ${toString value}";
|
||||
|
||||
renderAttrs =
|
||||
indent: attrs:
|
||||
lib.pipe attrs [
|
||||
lib.attrsToList
|
||||
(map ({ name, value }: renderLine indent name value))
|
||||
(lib.filter (line: line != null))
|
||||
(lib.concatStringsSep "\n")
|
||||
];
|
||||
|
||||
renderSectionName = name: "[${lib.escape [ "[" "]" ] name}]";
|
||||
|
||||
renderSection = name: attrs: ''
|
||||
${renderSectionName name}
|
||||
${renderAttrs "" attrs}
|
||||
'';
|
||||
|
||||
renderSubsection =
|
||||
name: attrs:
|
||||
lib.optionalString (attrs != { }) ''
|
||||
${name}:
|
||||
${renderAttrs " " attrs}
|
||||
'';
|
||||
|
||||
renderVolume = urlPath: volume: ''
|
||||
${renderSectionName urlPath}
|
||||
${volume.path}
|
||||
${renderSubsection "accs" volume.access}
|
||||
${renderSubsection "flags" volume.flags}
|
||||
'';
|
||||
in
|
||||
{
|
||||
render =
|
||||
{
|
||||
accounts,
|
||||
global,
|
||||
volumes,
|
||||
globalExtraConfig ? "",
|
||||
}:
|
||||
''
|
||||
${renderSection "global" global}
|
||||
${globalExtraConfig}
|
||||
${renderSection "accounts" accounts}
|
||||
${lib.concatStringsSep "\n" (lib.mapAttrsToList renderVolume volumes)}
|
||||
'';
|
||||
}
|
||||
@@ -24,7 +24,6 @@ in
|
||||
configs/nixos/hosts/neogaia/hardware-configuration.nix \
|
||||
configs/nixos/hosts/pikachu/hardware-configuration.nix \
|
||||
configs/nixos/hosts/zeus/hardware-configuration.nix \
|
||||
src/natu/testdata \
|
||||
third_party \
|
||||
-- .
|
||||
treefmt
|
||||
|
||||
@@ -6,6 +6,19 @@
|
||||
system,
|
||||
}:
|
||||
{
|
||||
overlays.custom = final: _prev: {
|
||||
alexion = import ./packages {
|
||||
pkgs = final;
|
||||
inherit root;
|
||||
};
|
||||
};
|
||||
|
||||
packages.${system} =
|
||||
(import inputs.nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ self.overlays.custom ];
|
||||
}).alexion;
|
||||
|
||||
nixosConfigurations = import ../dotfiles {
|
||||
inherit
|
||||
inputs
|
||||
|
||||
@@ -6,7 +6,6 @@ inputs.treefmt-nix.lib.evalModule inputs.nixpkgs.legacyPackages.${system} {
|
||||
|
||||
settings.formatter.nixfmt.excludes = [
|
||||
"configs/nixos/hosts/*/hardware-configuration.nix"
|
||||
"src/natu/testdata/*.nix"
|
||||
"third_party/**"
|
||||
];
|
||||
}
|
||||
|
||||
8
src/flake/packages/default.nix
Normal file
8
src/flake/packages/default.nix
Normal file
@@ -0,0 +1,8 @@
|
||||
{ pkgs, root }:
|
||||
{
|
||||
pi-coding-agent = pkgs.callPackage ./pi-coding-agent.nix {
|
||||
inherit root;
|
||||
};
|
||||
|
||||
waybar = pkgs.callPackage ./waybar.nix { };
|
||||
}
|
||||
4
src/flake/packages/pi-coding-agent.nix
Normal file
4
src/flake/packages/pi-coding-agent.nix
Normal file
@@ -0,0 +1,4 @@
|
||||
{ pkgs, root }:
|
||||
pkgs.callPackage (root + "/third_party/pi/nix/package.nix") {
|
||||
source = root + "/third_party/pi";
|
||||
}
|
||||
28
src/flake/packages/waybar.nix
Normal file
28
src/flake/packages/waybar.nix
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
fetchFromGitHub,
|
||||
waybar,
|
||||
}:
|
||||
waybar.overrideAttrs {
|
||||
src = fetchFromGitHub {
|
||||
owner = "Alexays";
|
||||
repo = "Waybar";
|
||||
rev = "05945748dccce28bf96d26d8f64a9e69a8dd49ba";
|
||||
hash = "sha256-51R3mIt8cLNvh/X5qe9vOqeJCj0U9KRyemVE5y+OhiU=";
|
||||
};
|
||||
|
||||
postUnpack =
|
||||
let
|
||||
libcava = fetchFromGitHub {
|
||||
owner = "LukashonakV";
|
||||
repo = "cava";
|
||||
tag = "0.10.7";
|
||||
hash = "sha256-zkyj1vBzHtoypX4Bxdh1Vmwh967DKKxN751v79hzmgQ=";
|
||||
};
|
||||
in
|
||||
''
|
||||
pushd "$sourceRoot"
|
||||
cp -R --no-preserve=mode,ownership ${libcava} subprojects/cava-0.10.7
|
||||
patchShebangs .
|
||||
popd
|
||||
'';
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test")
|
||||
|
||||
rust_library(
|
||||
name = "natu_lib",
|
||||
crate_name = "natu",
|
||||
srcs = [
|
||||
"ast.rs",
|
||||
"file_store.rs",
|
||||
"natu.rs",
|
||||
"parser.rs",
|
||||
"rules.rs",
|
||||
"rules/let_order.rs",
|
||||
"rules/list_to_attrs_map.rs",
|
||||
"rules/manual_inherit.rs",
|
||||
"rules/manual_inherit_from.rs",
|
||||
"rules/redundant_let.rs",
|
||||
"rules/redundant_parameter_forwarding.rs",
|
||||
"rules/unquoted_uri.rs",
|
||||
"scope_graph.rs",
|
||||
],
|
||||
deps = [
|
||||
"//src/status:status",
|
||||
"//third_party/crates:rnix",
|
||||
"//third_party/crates/rowan-0.16.1:rowan",
|
||||
],
|
||||
)
|
||||
|
||||
rust_test(
|
||||
name = "file_store_test",
|
||||
srcs = ["file_store_test.rs"],
|
||||
deps = [":natu_lib"],
|
||||
)
|
||||
|
||||
rust_test(
|
||||
name = "natu_test",
|
||||
srcs = ["natu_test.rs"],
|
||||
data = [
|
||||
"testdata/clean.nix",
|
||||
"testdata/invalid.nix",
|
||||
"testdata/lint_findings.nix",
|
||||
],
|
||||
deps = [
|
||||
":natu_lib",
|
||||
"//src/status:status",
|
||||
"//third_party/crates:googletest",
|
||||
],
|
||||
)
|
||||
|
||||
rust_binary(
|
||||
name = "natu",
|
||||
crate_name = "natu_cli",
|
||||
srcs = ["natu_main.rs"],
|
||||
deps = [
|
||||
":natu_lib",
|
||||
"//src/monorepo:monorepo",
|
||||
"//src/status:status",
|
||||
"//third_party/crates:clap",
|
||||
],
|
||||
)
|
||||
@@ -1,21 +0,0 @@
|
||||
[package]
|
||||
name = "natu"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
path = "natu.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "natu"
|
||||
path = "natu_main.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
monorepo = { workspace = true }
|
||||
rnix = { workspace = true }
|
||||
rowan = { workspace = true }
|
||||
status = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
googletest = { workspace = true }
|
||||
@@ -1,92 +0,0 @@
|
||||
use crate::FileId;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParsedFile {
|
||||
pub file_id: FileId,
|
||||
pub root: Expr,
|
||||
pub nodes: Vec<Node>,
|
||||
pub tokens: Vec<Token>,
|
||||
pub errors: Vec<ParseError>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Node {
|
||||
pub range: SourceRange,
|
||||
pub data: NodeData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct SourceRange {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SourceRange {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(formatter, "{}..{}", self.start, self.end)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum NodeData {
|
||||
Binding(Binding),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Binding {
|
||||
pub range: SourceRange,
|
||||
pub key: Ident,
|
||||
pub value: Expr,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Ident {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Expr {
|
||||
Ident(Ident),
|
||||
Select(Select),
|
||||
Apply(Apply),
|
||||
Let {
|
||||
range: SourceRange,
|
||||
bindings: Vec<Binding>,
|
||||
body: Box<Expr>,
|
||||
},
|
||||
AttrSet(Vec<Binding>),
|
||||
Lambda {
|
||||
parameters: Vec<Ident>,
|
||||
body: Box<Expr>,
|
||||
},
|
||||
Other(Vec<Expr>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Select {
|
||||
pub receiver: String,
|
||||
pub attr: Ident,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Apply {
|
||||
pub lambda: Box<Expr>,
|
||||
pub argument: Box<Expr>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Token {
|
||||
pub range: SourceRange,
|
||||
pub data: TokenData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TokenData {
|
||||
UnquotedUri(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ParseError {
|
||||
pub range: SourceRange,
|
||||
pub message: String,
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct FileStore {
|
||||
files: Vec<SourceFile>,
|
||||
}
|
||||
|
||||
impl FileStore {
|
||||
pub fn add(&mut self, file: SourceFile) -> FileId {
|
||||
let id = FileId(self.files.len());
|
||||
self.files.push(file);
|
||||
id
|
||||
}
|
||||
|
||||
pub fn get(&self, id: FileId) -> &SourceFile {
|
||||
&self.files[id.0]
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<SourceFile>> for FileStore {
|
||||
fn from(files: Vec<SourceFile>) -> Self {
|
||||
Self { files }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SourceFile {
|
||||
pub path: PathBuf,
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct FileId(usize);
|
||||
@@ -1,33 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use natu::{FileStore, SourceFile};
|
||||
|
||||
fn source_file(path: &str, source: &str) -> SourceFile {
|
||||
SourceFile {
|
||||
path: PathBuf::from(path),
|
||||
source: source.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn added_file_can_be_retrieved_by_id() {
|
||||
let mut store = FileStore::default();
|
||||
|
||||
let id = store.add(source_file("first.nix", "{ inherit foo; }"));
|
||||
let file = store.get(id);
|
||||
|
||||
assert_eq!(PathBuf::from("first.nix"), file.path);
|
||||
assert_eq!("{ inherit foo; }", file.source);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_ids_refer_to_their_added_files() {
|
||||
let mut store = FileStore::default();
|
||||
|
||||
let first = store.add(source_file("first.nix", "first"));
|
||||
let second = store.add(source_file("second.nix", "second"));
|
||||
|
||||
assert_ne!(first, second);
|
||||
assert_eq!("first", store.get(first).source);
|
||||
assert_eq!("second", store.get(second).source);
|
||||
}
|
||||
121
src/natu/natu.rs
121
src/natu/natu.rs
@@ -1,121 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
mod ast;
|
||||
mod file_store;
|
||||
mod parser;
|
||||
mod rules;
|
||||
mod scope_graph;
|
||||
|
||||
use scope_graph::ScopeGraph;
|
||||
|
||||
pub use file_store::{FileId, FileStore, SourceFile};
|
||||
pub use rules::{Diagnostic, Finding};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Report {
|
||||
pub files: FileStore,
|
||||
pub diagnostics: Vec<Diagnostic>,
|
||||
}
|
||||
|
||||
impl Report {
|
||||
pub fn is_clean(&self) -> bool {
|
||||
self.diagnostics.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Report {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut reported_let_order_files = HashSet::new();
|
||||
|
||||
for diagnostic in &self.diagnostics {
|
||||
if diagnostic.finding.rule_id == "let_order" {
|
||||
if !reported_let_order_files.insert(diagnostic.file_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let file = self.files.get(diagnostic.file_id);
|
||||
writeln!(
|
||||
formatter,
|
||||
"{} [let_order] Let bindings must follow dependency order",
|
||||
file.path.display()
|
||||
)?;
|
||||
|
||||
let mut findings = self
|
||||
.diagnostics
|
||||
.iter()
|
||||
.filter(|candidate| {
|
||||
candidate.file_id == diagnostic.file_id
|
||||
&& candidate.finding.rule_id == "let_order"
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
findings.sort_by_key(|finding| finding.finding.range.start);
|
||||
|
||||
for finding in findings {
|
||||
let (start, _) = line_range(&file.source, finding.finding.range);
|
||||
let expected_order =
|
||||
finding.finding.expected_order.as_ref().expect("let order");
|
||||
writeln!(
|
||||
formatter,
|
||||
" let at line {start}: {}",
|
||||
expected_order.join(", ")
|
||||
)?;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
let file = self.files.get(diagnostic.file_id);
|
||||
let (start, end) = line_range(&file.source, diagnostic.finding.range);
|
||||
let location = if start == end {
|
||||
start.to_string()
|
||||
} else {
|
||||
format!("{start}-{end}")
|
||||
};
|
||||
writeln!(formatter, "{}:{location} {diagnostic}", file.path.display())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn line_range(source: &str, range: ast::SourceRange) -> (usize, usize) {
|
||||
let line_at = |offset| {
|
||||
source
|
||||
.bytes()
|
||||
.take(offset)
|
||||
.filter(|byte| *byte == b'\n')
|
||||
.count()
|
||||
+ 1
|
||||
};
|
||||
let start = line_at(range.start);
|
||||
let end = line_at(range.end);
|
||||
|
||||
(start, end)
|
||||
}
|
||||
|
||||
/// Analyzes source files and returns the diagnostics Natu finds.
|
||||
pub fn future_sight(files: Vec<SourceFile>) -> status::Result<Report> {
|
||||
let mut store = FileStore::default();
|
||||
let mut diagnostics = Vec::new();
|
||||
|
||||
for file in files {
|
||||
let file_id = store.add(file);
|
||||
let file = store.get(file_id);
|
||||
let parsed = parser::parse_file(file_id, &file.source);
|
||||
|
||||
if !parsed.errors.is_empty() {
|
||||
return Err(status::Status::failed_precondition(format!(
|
||||
"failed to parse {}",
|
||||
file.path.display()
|
||||
)));
|
||||
}
|
||||
|
||||
let graph = ScopeGraph::from_file(&parsed);
|
||||
diagnostics.extend(rules::run_rules(&parsed, &graph, &file.source));
|
||||
}
|
||||
|
||||
Ok(Report {
|
||||
files: store,
|
||||
diagnostics,
|
||||
})
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
use clap::Parser;
|
||||
use natu::{Report, SourceFile};
|
||||
use status::ResultExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitCode;
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args = Args::parse();
|
||||
|
||||
match analyze(args) {
|
||||
Ok(report) if report.is_clean() => {
|
||||
println!("No findings.");
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Ok(report) => {
|
||||
print!("{report}");
|
||||
ExitCode::from(1)
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("{error}");
|
||||
ExitCode::from(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct Args {
|
||||
#[arg(value_name = "PATH", default_value = ".")]
|
||||
paths: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
fn analyze(args: Args) -> status::Result<Report> {
|
||||
let paths: Vec<PathBuf> = args
|
||||
.paths
|
||||
.into_iter()
|
||||
.map(monorepo::workspace::resolve_workspace_path)
|
||||
.collect();
|
||||
|
||||
let files: Vec<SourceFile> = paths
|
||||
.iter()
|
||||
.map(|path| collect_source_files(path))
|
||||
.collect::<status::Result<Vec<_>>>()?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
natu::future_sight(files)
|
||||
}
|
||||
|
||||
fn collect_source_files(path: &Path) -> status::Result<Vec<SourceFile>> {
|
||||
let metadata = std::fs::symlink_metadata(path)
|
||||
.with_context(|| format!("failed to read metadata for {}", path.display()))?;
|
||||
|
||||
if metadata.file_type().is_dir() {
|
||||
if is_excluded_dir(path) {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut files = Vec::new();
|
||||
|
||||
for entry in std::fs::read_dir(path)
|
||||
.with_context(|| format!("failed to read directory {}", path.display()))?
|
||||
{
|
||||
let entry =
|
||||
entry.with_context(|| format!("failed to read entry in {}", path.display()))?;
|
||||
files.extend(collect_source_files(&entry.path())?);
|
||||
}
|
||||
|
||||
return Ok(files);
|
||||
}
|
||||
|
||||
if metadata.file_type().is_file() && is_nix_file(path) {
|
||||
return Ok(vec![SourceFile {
|
||||
path: path.to_path_buf(),
|
||||
source: std::fs::read_to_string(path)
|
||||
.with_context(|| format!("failed to read source file {}", path.display()))?,
|
||||
}]);
|
||||
}
|
||||
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
// TODO: Add configurable exclusions.
|
||||
fn is_excluded_dir(path: &Path) -> bool {
|
||||
path.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.is_some_and(|name| {
|
||||
matches!(
|
||||
name,
|
||||
".git"
|
||||
| "bazel-bin"
|
||||
| "bazel-dev"
|
||||
| "bazel-out"
|
||||
| "bazel-testlogs"
|
||||
| "target"
|
||||
| "testdata"
|
||||
| "third_party"
|
||||
) || name == "result"
|
||||
|| name.starts_with("result-")
|
||||
})
|
||||
}
|
||||
|
||||
fn is_nix_file(path: &Path) -> bool {
|
||||
path.extension().is_some_and(|extension| extension == "nix")
|
||||
}
|
||||
@@ -1,303 +0,0 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use googletest::matchers::{contains, eq, is_true};
|
||||
use googletest::{OrFail, expect_that, gtest};
|
||||
use status::Code;
|
||||
use natu::SourceFile;
|
||||
|
||||
fn fixture_source(path: &str) -> googletest::Result<SourceFile> {
|
||||
Ok(SourceFile {
|
||||
path: PathBuf::from(path),
|
||||
source: std::fs::read_to_string(fixture_path(path)?).or_fail()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn fixture_path(path: &str) -> googletest::Result<PathBuf> {
|
||||
if Path::new(path).exists() {
|
||||
return Ok(PathBuf::from(path));
|
||||
}
|
||||
|
||||
let runfiles = PathBuf::from(std::env::var("TEST_SRCDIR").or_fail()?);
|
||||
let workspace = std::env::var("TEST_WORKSPACE").or_fail()?;
|
||||
|
||||
Ok(runfiles.join(workspace).join("src/natu").join(path))
|
||||
}
|
||||
|
||||
#[gtest]
|
||||
fn natu_accepts_clean_source() -> googletest::Result<()> {
|
||||
let report = natu::future_sight(vec![fixture_source("testdata/clean.nix")?]).or_fail()?;
|
||||
|
||||
expect_that!(report.is_clean(), is_true());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[gtest]
|
||||
fn natu_rejects_invalid_source() -> googletest::Result<()> {
|
||||
let error = natu::future_sight(vec![fixture_source("testdata/invalid.nix")?])
|
||||
.err()
|
||||
.or_fail()?;
|
||||
|
||||
expect_that!(error.code(), eq(Code::FailedPrecondition));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[gtest]
|
||||
fn natu_reports_out_of_order_let_bindings() -> googletest::Result<()> {
|
||||
let report = natu::future_sight(vec![fixture_source("testdata/lint_findings.nix")?]).or_fail()?;
|
||||
|
||||
let let_order_count = report
|
||||
.diagnostics
|
||||
.iter()
|
||||
.filter(|diagnostic| diagnostic.finding.rule_id == "let_order")
|
||||
.count();
|
||||
expect_that!(let_order_count, eq(2));
|
||||
expect_that!(
|
||||
report.to_string(),
|
||||
eq(concat!(
|
||||
"testdata/lint_findings.nix:11 [manual_inherit] Use `inherit` for this assignment\n",
|
||||
"testdata/lint_findings.nix:12 [manual_inherit_from] Use `inherit` for this selected attribute\n",
|
||||
"testdata/lint_findings.nix:13 [unquoted_uri] URI literals should be quoted\n",
|
||||
"testdata/lint_findings.nix [let_order] Let bindings must follow dependency order\n",
|
||||
" let at line 1: first, second, nested\n",
|
||||
" let at line 4: nestedSecond, nestedFirst\n",
|
||||
))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[gtest]
|
||||
fn natu_preserves_suppressed_bindings() -> googletest::Result<()> {
|
||||
let report = natu::future_sight(vec![SourceFile {
|
||||
path: PathBuf::from("testdata/nolint.nix"),
|
||||
source: "{ config, ... }: let # NOLINT(redundant_let)\n cfg = config.example; # NOLINT(let_order)\n value = cfg;\nin value\n".into(),
|
||||
}])
|
||||
.or_fail()?;
|
||||
|
||||
expect_that!(report.is_clean(), is_true());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[gtest]
|
||||
fn natu_reports_select_dependency_order() -> googletest::Result<()> {
|
||||
let report = natu::future_sight(vec![SourceFile {
|
||||
path: PathBuf::from("testdata/select_dependencies.nix"),
|
||||
source: r#"
|
||||
let
|
||||
dependency = { value = 1; };
|
||||
entry = dependency.value;
|
||||
in
|
||||
entry
|
||||
"#
|
||||
.into(),
|
||||
}])
|
||||
.or_fail()?;
|
||||
|
||||
let count = report
|
||||
.diagnostics
|
||||
.iter()
|
||||
.filter(|diagnostic| diagnostic.finding.rule_id == "let_order")
|
||||
.count();
|
||||
expect_that!(count, eq(1));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[gtest]
|
||||
fn natu_reports_redundant_let() -> googletest::Result<()> {
|
||||
let report = natu::future_sight(vec![SourceFile {
|
||||
path: PathBuf::from("testdata/redundant_let.nix"),
|
||||
source: r#"
|
||||
let
|
||||
helper = 1;
|
||||
value = helper;
|
||||
in
|
||||
value
|
||||
"#
|
||||
.into(),
|
||||
}])
|
||||
.or_fail()?;
|
||||
|
||||
let count = report
|
||||
.diagnostics
|
||||
.iter()
|
||||
.filter(|diagnostic| diagnostic.finding.rule_id == "redundant_let")
|
||||
.count();
|
||||
expect_that!(count, eq(1));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[gtest]
|
||||
fn natu_accepts_nonredundant_let() -> googletest::Result<()> {
|
||||
let report = natu::future_sight(vec![
|
||||
SourceFile {
|
||||
path: PathBuf::from("testdata/recursive_let.nix"),
|
||||
source: r#"
|
||||
let
|
||||
recursive = recursive;
|
||||
in
|
||||
recursive
|
||||
"#
|
||||
.into(),
|
||||
},
|
||||
SourceFile {
|
||||
path: PathBuf::from("testdata/dependent_let.nix"),
|
||||
source: r#"
|
||||
let
|
||||
value = 1;
|
||||
dependent = value;
|
||||
in
|
||||
value
|
||||
"#
|
||||
.into(),
|
||||
},
|
||||
])
|
||||
.or_fail()?;
|
||||
|
||||
let count = report
|
||||
.diagnostics
|
||||
.iter()
|
||||
.filter(|diagnostic| diagnostic.finding.rule_id == "redundant_let")
|
||||
.count();
|
||||
expect_that!(count, eq(0));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[gtest]
|
||||
fn natu_reports_redundant_parameter_forwarding() -> googletest::Result<()> {
|
||||
let report = natu::future_sight(vec![
|
||||
SourceFile {
|
||||
path: PathBuf::from("testdata/attribute_parameter.nix"),
|
||||
source: r#"
|
||||
{ hosts, ... }:
|
||||
let
|
||||
collect = { hosts }: hosts;
|
||||
in
|
||||
collect { inherit hosts; }
|
||||
"#
|
||||
.into(),
|
||||
},
|
||||
SourceFile {
|
||||
path: PathBuf::from("testdata/plain_parameter.nix"),
|
||||
source: r#"
|
||||
value:
|
||||
let
|
||||
transform = value: value;
|
||||
in
|
||||
transform value
|
||||
"#
|
||||
.into(),
|
||||
},
|
||||
SourceFile {
|
||||
path: PathBuf::from("testdata/curried_parameter.nix"),
|
||||
source: r#"
|
||||
{ hosts, ... }:
|
||||
let
|
||||
transform = prefix: hosts: suffix: hosts;
|
||||
in
|
||||
transform "prefix" hosts "suffix"
|
||||
"#
|
||||
.into(),
|
||||
},
|
||||
SourceFile {
|
||||
path: PathBuf::from("testdata/local_parameter.nix"),
|
||||
source: r#"
|
||||
let
|
||||
transform = value: value;
|
||||
in
|
||||
transform value
|
||||
"#
|
||||
.into(),
|
||||
},
|
||||
])
|
||||
.or_fail()?;
|
||||
|
||||
let count = report
|
||||
.diagnostics
|
||||
.iter()
|
||||
.filter(|diagnostic| diagnostic.finding.rule_id == "redundant_parameter_forwarding")
|
||||
.count();
|
||||
expect_that!(count, eq(3));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[gtest]
|
||||
fn natu_reports_list_to_attrs_after_map() -> googletest::Result<()> {
|
||||
let report = natu::future_sight(vec![SourceFile {
|
||||
path: PathBuf::from("testdata/list_to_attrs.nix"),
|
||||
source: r#"
|
||||
let
|
||||
generated = lib.listToAttrs (
|
||||
map (item: lib.nameValuePair item item) items
|
||||
);
|
||||
generated_pairs = lib.listToAttrs (
|
||||
concatLists (
|
||||
mapAttrsToList (name: _: lib.nameValuePair name name) attrs
|
||||
)
|
||||
);
|
||||
precomputed = lib.listToAttrs pairs;
|
||||
in
|
||||
{
|
||||
inherit generated generated_pairs precomputed;
|
||||
}
|
||||
"#
|
||||
.into(),
|
||||
}])
|
||||
.or_fail()?;
|
||||
|
||||
let count = report
|
||||
.diagnostics
|
||||
.iter()
|
||||
.filter(|diagnostic| diagnostic.finding.rule_id == "list_to_attrs_map")
|
||||
.count();
|
||||
expect_that!(count, eq(1));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[gtest]
|
||||
fn natu_reports_unquoted_uri() -> googletest::Result<()> {
|
||||
let report = natu::future_sight(vec![fixture_source("testdata/lint_findings.nix")?]).or_fail()?;
|
||||
|
||||
let findings = report
|
||||
.diagnostics
|
||||
.iter()
|
||||
.map(|diagnostic| {
|
||||
(
|
||||
report.files.get(diagnostic.file_id).path.clone(),
|
||||
diagnostic.finding.rule_id,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
expect_that!(
|
||||
findings,
|
||||
contains(eq(&(
|
||||
PathBuf::from("testdata/lint_findings.nix"),
|
||||
"unquoted_uri"
|
||||
)))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[gtest]
|
||||
fn natu_reports_manual_inherit() -> googletest::Result<()> {
|
||||
let report = natu::future_sight(vec![fixture_source("testdata/lint_findings.nix")?]).or_fail()?;
|
||||
|
||||
let manual_inherit_count = report
|
||||
.diagnostics
|
||||
.iter()
|
||||
.filter(|diagnostic| diagnostic.finding.rule_id == "manual_inherit")
|
||||
.count();
|
||||
expect_that!(manual_inherit_count, eq(1));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[gtest]
|
||||
fn natu_reports_manual_inherit_from() -> googletest::Result<()> {
|
||||
let report = natu::future_sight(vec![fixture_source("testdata/lint_findings.nix")?]).or_fail()?;
|
||||
|
||||
let manual_inherit_from_count = report
|
||||
.diagnostics
|
||||
.iter()
|
||||
.filter(|diagnostic| diagnostic.finding.rule_id == "manual_inherit_from")
|
||||
.count();
|
||||
expect_that!(manual_inherit_from_count, eq(1));
|
||||
Ok(())
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user