Modular, context-aware and aspect-oriented dendritic Nix configurations. Discussions: https://oeiuwq.zulipchat.com/join/nqp26cd4kngon6mo3ncgnuap/ den.oeiuwq.com
configurations den dendritic nix aspect oriented

default template includes routes.nix example and tests with namespaces and angle-brackets. (#80)

This also removes outdated `_profile` directory. Closes #78. Instead our
default template now includes an `eg/routes.nix` example router and
exercises namespaces and angle-brackets, ensuring via tests that they
work.

authored by oeiuwq.com and committed by

GitHub 5e22f73c f8dff77c

+137 -236
+22 -10
README.md
··· 18 18 19 19 <img width="300" height="300" alt="den" src="https://github.com/user-attachments/assets/af9c9bca-ab8b-4682-8678-31a70d510bbb" /> 20 20 21 - - Dendritic: same concern, different classes and context-aware. 21 + - Dendritic: each module configures **same** concern over **different** Nix classes. 22 22 23 - - Small, [DRY](modules/aspects/provides/unfree.nix) & [`class`-generic](modules/aspects/provides/primary-user.nix) modules. 23 + - Create [DRY](modules/aspects/provides/unfree.nix) & [`class`-generic](modules/aspects/provides/primary-user.nix) modules. 24 24 25 25 - [Parametric](modules/aspects/provides/define-user.nix) over `host`/`home`/`user`. 26 26 27 - - [Share](templates/examples/modules/_profile/namespace.nix) aspects across systems & repos. 27 + - [Share](templates/default/modules/namespace.nix) aspects across systems & repos. 28 28 29 - - Bidirectional [dependencies](modules/aspects/dependencies.nix): user/host contributions. 29 + - Context-aware [dependencies](modules/aspects/dependencies.nix): user/host contributions. 30 + 31 + - [Routable](templates/default/modules/aspects/eg/routes.nix) configurations. 30 32 31 33 - Custom factories for any Nix `class`. 32 34 ··· 38 40 39 41 - [Batteries](modules/aspects/provides/): Opt-in, replaceable aspects. 40 42 41 - - [Well-tested](templates/examples/modules/_example/ci) with [examples](templates/examples). 43 + - Opt-in [`<angle/brackets>`](https://vic.github.io/den/angle-brackets.html) aspect resolution. 44 + 45 + - Templates [tested](templates/default/modules/tests.nix) along [examples](templates/examples/modules/_example/ci). 46 + 47 + - Concepts [documented](https://vic.github.io/den). 42 48 43 49 Need more batteries? See [vic/denful](https://github.com/vic/denful). 44 50 ··· 52 58 53 59 ```nix 54 60 # modules/hosts.nix 55 - # OS & standalone homes share 'vic' aspect. 56 - # $ nixos-rebuild switch --flake .#my-laptop 57 - # $ home-manager switch --flake .#vic 58 61 { 59 - den.hosts.x86-64-linux.laptop.users.vic = {}; 62 + # same home-manager vic configuration 63 + # over laptop, macbook and standalone-hm 64 + den.hosts.x86_64-linux.lap.users.vic = {}; 65 + den.hosts.aarch64-darwin.mac.users.vic = {}; 60 66 den.homes.aarch64-darwin.vic = {}; 61 67 } 68 + ``` 69 + 70 + ```console 71 + $ nixos-rebuild switch --flake .#lap 72 + $ darwin-rebuild switch --flake .#mac 73 + $ home-manager switch --flake .#vic 62 74 ``` 63 75 64 76 🧩 [Aspect-oriented](https://github.com/vic/flake-aspects) incremental features. ([example](templates/default/modules/den.nix)) ··· 87 99 # User contribs to host 88 100 nixos.users.users = { 89 101 vic.description = "oeiuwq"; 90 - } 102 + }; 91 103 includes = [ 92 104 den.aspects.tiling-wm 93 105 den._.primary-user
+12 -7
nix/den-brackets.nix
··· 1 1 # __findFile implementation to resolve deep aspects. 2 2 # inspired by https://fzakaria.com/2025/08/10/angle-brackets-in-a-nix-flake-world 3 - # 4 - # For user facing documentation, see: 5 - # See templates/default/_profile/den-brackets.nix 6 - # See templates/default/_profile/namespace.nix 7 3 { 8 4 lib, 9 5 config, ··· 21 17 notFound = "Aspect not found: ${lib.concatStringsSep "." path}"; 22 18 23 19 headIsDen = head == "den"; 24 - readFromDen = lib.getAttrFromPath tail config.den; 20 + readFromDen = lib.getAttrFromPath ([ "den" ] ++ tail) config; 25 21 26 22 headIsAspect = builtins.hasAttr head config.den.aspects; 27 - readFromAspects = lib.getAttrFromPath path config.den.aspects; 23 + aspectsPath = [ 24 + "den" 25 + "aspects" 26 + ] ++ path; 27 + readFromAspects = lib.getAttrFromPath aspectsPath config; 28 28 29 29 headIsDenful = lib.hasAttrByPath [ "ful" head ] config.den; 30 30 denfulTail = if lib.head tail == "provides" then lib.tail tail else tail; 31 - readFromDenful = lib.getAttrFromPath ([ head ] ++ denfulTail) config.den.ful; 31 + denfulPath = [ 32 + "den" 33 + "ful" 34 + head 35 + ] ++ denfulTail; 36 + readFromDenful = lib.getAttrFromPath denfulPath config; 32 37 33 38 found = 34 39 if headIsDen then
+7
templates/default/modules/aspects/alice.nix
··· 30 30 { 31 31 home.packages = [ pkgs.htop ]; 32 32 }; 33 + 34 + # <user>.provides.<host>, via eg/routes.nix 35 + provides.igloo = 36 + { host, ... }: 37 + { 38 + nixos.programs.nh.enable = host.name == "igloo"; 39 + }; 33 40 }; 34 41 }
+3
templates/default/modules/aspects/defaults.nix
··· 16 16 17 17 # These are functions that produce configs 18 18 den.default.includes = [ 19 + # ${user}.provides.${host} and ${host}.provides.${user} 20 + <eg/routes> 21 + 19 22 # Enable home-manager on all hosts. 20 23 <den/home-manager> 21 24
+53
templates/default/modules/aspects/eg/routes.nix
··· 1 + # This example implements an aspect "routing" pattern. 2 + # 3 + # Unlike `den.default` which is `parametric.atLeast` we use `parametric.exactly` here 4 + # to be more strict and prevent multiple values inclusion. 5 + # 6 + # Be sure to read: https://vic.github.io/den/dependencies.html 7 + # See usage at: defaults.nix, alice.nix, igloo.nix 8 + # 9 + { den, eg, ... }: 10 + { 11 + # Usage: `den.default.includes [ eg.routes ]` 12 + eg.routes = 13 + let 14 + inherit (den.lib) parametric; 15 + 16 + os-from-user = 17 + { 18 + user, 19 + host, 20 + # deadnix: skip 21 + OS, 22 + # deadnix: skip 23 + fromUser, 24 + }: 25 + parametric { inherit user host; } (mutual user host); 26 + 27 + hm-from-host = 28 + { 29 + user, 30 + host, 31 + # deadnix: skip 32 + HM, 33 + # deadnix: skip 34 + fromHost, 35 + }: 36 + parametric { inherit user host; } (mutual host user); 37 + 38 + mutual = from: to: { 39 + includes = [ 40 + # eg, `<user>._.<host>` and `<host>._.<user>` 41 + (den.aspects.${from.aspect}._.${to.aspect} or { }) 42 + ]; 43 + }; 44 + 45 + in 46 + { 47 + __functor = parametric.exactly; 48 + includes = [ 49 + os-from-user 50 + hm-from-host 51 + ]; 52 + }; 53 + }
+7
templates/default/modules/aspects/igloo.nix
··· 16 16 eg.vm-bootable 17 17 eg.xfce-desktop 18 18 ]; 19 + 20 + # <host>.provides.<user>, via eg/routes.nix 21 + provides.alice = 22 + { user, ... }: 23 + { 24 + homeManager.programs.helix.enable = user.name == "alice"; 25 + }; 19 26 }; 20 27 }
+25
templates/default/modules/tests.nix
··· 1 + # Some CI checks to ensure this template always works. 2 + # Feel free to adapt or remove when this repo is yours. 3 + { inputs, ... }: 4 + { 5 + perSystem = 6 + { pkgs, self', ... }: 7 + let 8 + checkCond = name: cond: pkgs.runCommandLocal name { } (if cond then "touch $out" else ""); 9 + apple = inputs.self.darwinConfigurations.apple.config; 10 + igloo = inputs.self.nixosConfigurations.igloo.config; 11 + alice-at-igloo = igloo.home-manager.users.alice; 12 + vmBuilds = !pkgs.stdenvNoCC.isLinux || builtins.pathExists (self'.packages.vm + "/bin/vm"); 13 + iglooBuilds = !pkgs.stdenvNoCC.isLinux || builtins.pathExists (igloo.system.build.toplevel); 14 + appleBuilds = !pkgs.stdenvNoCC.isDarwin || builtins.pathExists (apple.system.build.toplevel); 15 + in 16 + { 17 + checks."igloo builds" = checkCond "igloo-builds" iglooBuilds; 18 + checks."apple builds" = checkCond "apple-builds" appleBuilds; 19 + checks."vm builds" = checkCond "vm-builds" vmBuilds; 20 + 21 + checks."alice enabled igloo nh" = checkCond "alice.provides.igloo" igloo.programs.nh.enable; 22 + checks."igloo enabled alice helix" = 23 + checkCond "igloo.provides.alice" alice-at-igloo.programs.helix.enable; 24 + }; 25 + }
+2 -12
templates/examples/modules/_example/README.md
··· 1 1 User TODO: REMOVE this directory (or disable its import from den.nix) 2 2 3 - It is used to implement tests for all den feature so we can validate at CI. 4 - 5 - Use it as reference to see how den features are used, 6 - however, be aware that this might not be the best practices for file/aspect 7 - organization. 8 - 9 - For a more "real-world" layout, see the `_profile` directory 10 - which is somewhat inspired on the 11 - dendritic implementation at [vic/vix](https://github.com/vic/vix/tree/8c8c7b8). 3 + It is used to implement tests for all den features so we can validate at CI. 12 4 13 - However, feel free to also not use any predefined layout, explore by yourself 14 - and find out how things work for you. Be sure to share your insights with 15 - the [community](https://github.com/vic/den/discussions) 5 + Use it as reference to see how den features are used, however, be aware that this might not be the best practices for file/aspect organization.
+1 -1
templates/examples/modules/_example/ci/import-tree.nix
··· 1 1 # configures class-automatic module auto imports for hosts/users/homes. 2 2 # See documentation at modules/aspects/provides/import-tree.nix 3 3 { 4 - # deadnix: skip # see _profile/den-brackets.nix 4 + # deadnix: skip 5 5 __findFile ? __findFile, 6 6 ... 7 7 }:
+5
templates/examples/modules/_example/ci/namespace.nix
··· 1 + { inputs, den, ... }: 2 + { 3 + imports = [ (inputs.den.namespace "eg" false) ]; 4 + _module.args.__findFile = den.lib.__findFile; 5 + }
-12
templates/examples/modules/_profile/README.md
··· 1 - User TODO: REMOVE this directory (or disable its import from den.nix). 2 - Move any module you find useful from here into your /modules directory. 3 - 4 - This directory contains a bare-bones layout for using den. 5 - It is inspired on the patterns shown in the 6 - dendritic implementation at [vic/vix](https://github.com/vic/vix/tree/den). 7 - 8 - Use it as reference of how to organize things. 9 - 10 - Feel free to adapt the layout, explore by yourself 11 - and find out how things work for you. Be sure to share your insights with 12 - the [community](https://github.com/vic/den/discussions)
-36
templates/examples/modules/_profile/den-brackets.nix
··· 1 - # This enables den's angle brackets opt-in feature. 2 - # Remove this file to opt-out. 3 - # 4 - # When den.lib.__findFile is in scope, you can do: 5 - # 6 - # <pro/foo/bar> and it will resolve to: 7 - # den.aspects.pro.provides.foo.provides.bar 8 - # 9 - # <pro/foo.includes> resolves to: 10 - # den.aspects.pro.provides.foo.includes 11 - # 12 - # <den/import-tree/home> resolves to: 13 - # den.provides.import-tree.provides.home 14 - # 15 - # <den.default> resolves to den.default 16 - # 17 - # When the vix remote namespace is enabled 18 - # <vix/foo> resolves to: den.ful.vix.provides.foo 19 - # 20 - # Usage: 21 - # 22 - # Bring `__findFile` into scope from module args: 23 - # 24 - # { __findFile, ... }: 25 - # den.default.includes = [ <den/home-manager> ]; 26 - # } 27 - # 28 - # IF you are using nixf-diagnose, it will complain 29 - # about __findFile not being used, trick it with: 30 - # 31 - # { __findFile ? __findFile, ... } 32 - # 33 - { den, ... }: 34 - { 35 - _module.args.__findFile = den.lib.__findFile; 36 - }
-3
templates/examples/modules/_profile/hosts.nix
··· 1 - { 2 - den.hosts.x86_64-linux.bones.users.fido = { }; 3 - }
-19
templates/examples/modules/_profile/hosts/bones/common-user-env.nix
··· 1 - # An aspect that contributes to any user home on the bones host. 2 - { ... }: 3 - let 4 - # private aspects can be let-bindings 5 - # more re-usable ones are better defined inside the `pro` namespace. 6 - host-contrib-to-user = 7 - { hostToUser, ... }: 8 - if hostToUser.host.name == "bones" || hostToUser.user.name == "fido" then 9 - { 10 - homeManager.programs.vim.enable = true; 11 - } 12 - else 13 - { }; 14 - in 15 - { 16 - den.default.includes = [ 17 - host-contrib-to-user 18 - ]; 19 - }
-67
templates/examples/modules/_profile/namespace.nix
··· 1 - # This module creates an aspect namespace. 2 - # 3 - # Just add the following import: 4 - # 5 - # # define local namespace. enable flake output. 6 - # imports = [ (inputs.den.namespace "vix" true) ]; 7 - # 8 - # # you can use remote namespaces and they will merge 9 - # imports = [ (inputs.den.namespace "vix" inputs.dendrix) ]; 10 - # 11 - # Internally, a namespace is just a `provides` branch: 12 - # 13 - # # den.ful is the social-convention for namespaces. 14 - # den.ful.<name> 15 - # 16 - # Having an aspect namespace is not required but helps a lot 17 - # with organization and conventient access to your aspects. 18 - # 19 - # The following examples use the `vix` namespace, 20 - # inspired by github:vic/vix own namespace pattern. 21 - # 22 - # By using an aspect namespace you can: 23 - # 24 - # - Directly write to aspects in your namespace. 25 - # 26 - # { 27 - # vix.gaming.nixos = ...; 28 - # 29 - # # instead of: 30 - # # den.ful.vix.gaming.nixos = ...; 31 - # } 32 - # 33 - # - Directly read aspects from your namespace. 34 - # 35 - # # Access the namespace from module args 36 - # { vix, ... }: 37 - # { 38 - # den.default.includes = [ vix.security ]; 39 - # 40 - # # instead of: 41 - # # den.default.includes = [ den.ful.vix.security ]; 42 - # } 43 - # 44 - # - Share and re-use aspects between Dendritic flakes 45 - # 46 - # # Aspects opt-in exposed as flake.denful.<name> 47 - # { imports = [( inputs.den.namespace "vix" true)] } 48 - # 49 - # # Many flakes can expose to the same namespace and we 50 - # # can merge them, accessing aspects in a uniform way. 51 - # { imports = [( inputs.den.namespace "vix" inputs.dendrix )] } 52 - # 53 - # - Use angle-brackets to access deeply nested trees 54 - # 55 - # # Be sure to read _profile/den-brackets.nix 56 - # { __findFile, ... }: 57 - # den.aspects.my-laptop.includes = [ <vix/gaming/retro> ]; 58 - # } 59 - # 60 - # 61 - # You can of course choose to not have any of the above. 62 - # USER TODO: Remove this file for not using a namespace. 63 - # USER TODO: Replace `pro` and update other files using it. 64 - { inputs, ... }: 65 - { 66 - imports = [ (inputs.den.namespace "pro" true) ]; 67 - }
-19
templates/examples/modules/_profile/profiles.nix
··· 1 - # Profiles are just aspects whose only job is to include other aspects 2 - # based on the properties (context) of the host/user they are included in. 3 - { pro, den, ... }: 4 - { 5 - 6 - # install profiles as parametric aspects on all hosts/users 7 - den.default.includes = [ 8 - pro.profiles 9 - ]; 10 - 11 - pro.profiles = { 12 - __functor = den.lib.parametric true; 13 - includes = [ 14 - ({ host, ... }: pro.${host.system} or { }) 15 - # add other routes according to context. 16 - ]; 17 - }; 18 - 19 - }
-14
templates/examples/modules/_profile/profiles/linux-utils-for-macos.nix
··· 1 - { 2 - 3 - # example custom profile per platform system, see profiles.nix 4 - pro.aarch64-darwin.darwin = 5 - { pkgs, ... }: 6 - { 7 - # provide a consistent environment with linux. 8 - environment.systemPackages = [ 9 - pkgs.coreutils 10 - pkgs.util-linux 11 - ]; 12 - }; 13 - 14 - }
-17
templates/examples/modules/_profile/profiles/single-user-is-admin.nix
··· 1 - { den, lib, ... }: 2 - { 3 - 4 - # When a host includes *ONLY* one user, make that user the admin. 5 - pro.single-user-is-admin = 6 - { userToHost, ... }@context: 7 - let 8 - inherit (userToHost) user host; 9 - single = 1 == builtins.length (builtins.attrValues host.users); 10 - exists = single && builtins.hasAttr user.name host.users; 11 - admin = lib.optionals exists [ den._.primary-user ]; 12 - in 13 - { 14 - __functor = den.lib.parametric context; 15 - includes = [ den._.define-user ] ++ admin; 16 - }; 17 - }
-16
templates/examples/modules/_profile/users/fido/common-host-env.nix
··· 1 - # An aspect that contributes to any operating system where fido is a user. 2 - # hooks itself into any host. 3 - { pro, ... }: 4 - let 5 - fido-at-host = 6 - { userToHost, ... }: 7 - if userToHost.user.name != "fido" then { } else pro.fido._.${userToHost.host.name}; 8 - in 9 - { 10 - den.default.includes = [ 11 - fido-at-host 12 - ]; 13 - 14 - # fido on bones host. 15 - pro.fido._.bones.nixos = { }; 16 - }
-3
templates/examples/modules/den.nix
··· 6 6 # The _example directory contains CI tests for all den features. 7 7 # use it as reference of usage, but not of best practices. 8 8 (inputs.import-tree ./_example) 9 - 10 - # The _profile directory contains a minimal profile-based layout. 11 - (inputs.import-tree ./_profile) 12 9 ]; 13 10 }