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
Nix 100.0%
93 4 8

Clone this repository

https://tangled.org/oeiuwq.com/den https://tangled.org/did:plc:hwcqoy35x55nzde2sm6dbvq7/den
git@tangled.org:oeiuwq.com/den git@tangled.org:did:plc:hwcqoy35x55nzde2sm6dbvq7/den

For self-hosted knots, clone URLs may differ based on your setup.

Download tar.gz
README.md

Sponsor Vic Dendritic Nix License CI Status

den - an aspect-oriented approach to Dendritic Nix configurations.#

den and vic's dendritic libs made for you with Love++ and AI--. If you like my work, consider sponsoring

den
  • Dendritic: Same concern over different classes.

  • Small and DRY
    class generic Nix configurations.

  • Context-Aware aspects.
    Parametric over host/home/user.

  • Stop copying and share(tm)
    aspects across systems and Dendritic repos.

  • Bidirectional configurations.
    Users can contribute to their Host configuration and the other way around.

  • Custom factories and output attributes.
    Support any new class of Nix configurations.

  • Use your stable/unstable input channels.

  • Freeform host/user/host schemas.
    Avoid the need for using specialArgs.

  • Multi-platform, Multi-tenant hosts.

  • Reuse aspects across hosts, OS, homes.

  • Batteries Included
    Opt-in and replaceable.

  • Well tested
    Suite exercises all features with examples.

Need more batteries? See vic/denful.

Join the community discussion. Ask questions, share how you use den.

🏠 Concise definitions of Hosts, Users and Standalone-Homes.

See _types.nix for complete schema.

# modules/hosts.nix
{
  # This example defines the following aspects:
  #  den.aspects.my-laptop and den.aspects.vic
  # standalone-hm and nixos-hm share vic aspect

  # $ nixos-rebuild switch --flake .#my-laptop
  den.hosts.x86-64-linux.my-laptop.users.vic = {}; 
  # $ home-manager switch --flake .#vic
  den.homes.aarch64-darwin.vic = { };

  # That's it! Now lets attach configs via aspects
}

🧩 Aspect-oriented configurations. (example)

# modules/my-laptop.nix -- Attach behaviour to host
{ den, ... }:
{
  den.aspects.my-laptop = {
    # dependency graph on other shared aspects
    includes = [
      # my parametric { host } => aspect
      den.aspects.vpn # setups firewall/daemons
      # opt-in, replaceable batteries included
      den.provides.home-manager
    ];
    # provide same features at any OS/platform
    nixos  = ...; # (see nixos options)
    darwin = ...; # (see nix-darwin options)
    # contrib hm-config to all my-laptop users
    homeManager.programs.vim.enable = true;
  };
}

# modules/vic.nix -- Reused in OS/standalone HM
{ den, ... }:
{
  den.aspects.vic = {
    homeManager = ...; # (see home-manager options)
    # user can contrib to hosts where it lives
    nixos.users.users.vic.description = "oeiuwq";
    includes = [ 
      den.aspects.tiling-wm 
      # parametric { user, host } => aspect
      den.provides.primary-user # vic is admin
    ];
  };
}

For real-world examples, see vic/vix or try this GH search.

❄️ Try it now! Launch our template VM:

nix run github:vic/den

Or clone it and run the VM as you edit

nix flake init -t github:vic/den
nix flake update den
nix run .#vm

Our default template provides a layout for quickstart.

You are done! You know everything there is to know about den for creating configurations with it.

However, if you want to learn more about how it works, I have tried to document some other topics in collapsible sections to avoid distraction from the introduction.

Basic Concepts and Patterns.#

Learn about aspects, static and parametric. Default aspects and dependencies.

There are two fundamental types of aspects in den, static and parametric.

Static aspects are just attribute sets#

# An aspect is a collection of many
# Nix configuration modules, each having
# a different `class`.
den.aspects.my-laptop = {
  nixos  = { };
  darwin = { };
  homeManager = { };
  nixVim = { };
  nixDroid = { };

  # aspects can be nested via `provides`
  # forming a tree structure.
  provides.gaming = {
    nixos = { };
    nixDroid = { };

    # aspects can also `include` others
    # forming a graph of dependencies
    includes = [
      # gaming.nixos module will mixin
      # nvidia-gpu.nixos module if any.
      den.aspects.nvidia-gpu
    ];
  };

};

TIP _ is an alias for provides. In many examples you will see foo._.bar._.baz instead of foo.provides.bar.provides.baz.

Parametric aspects are just functions.#

# The convention is to always use named args.
# These required args is the **context** the
# aspect needs to provide configuration.
hostParametric = { host }: {
  nixos.networking.hostName = host.hostName;

  # A parametric aspect can also ask other
  # aspects for context-aware configurations.
  # Here, we ask two other parametric aspects
  # given the `{ host, gaming }` context.
  includes = let 
    context.host = host;
    context.gaming.emulators = [ "nes" ];
  in map (f: f context) [
    den.default
    den.aspects.gaming-platform
  ];
};

Important context variants in den.#

Den uses the following contexts by default.

The aspect system is not limited to these,
but these are used to describe dependencies
between hosts/users, homes and default configs.

  • { host } - For host OS level configs.
  • { user, host } - For user Home level configs.
  • { home } - For standalone Home configs.

The Default aspect and default Dependencies#

Den has an special aspect at den.default that serves for global configuration. den.default is included by default in all Hosts, Users and Homes. For example a Home configuration invokes den.default { inherit home; }, to obtain the aggregated defaults for home contexts.

Registering defaults values#

{
  # you can assign static values directly.
  den.default.nixos = {
    system.stateVersion = "25.11";
  };

  # you can also include other aspects
  den.default.includes = [
    { darwin.system.stateVersion = 6; }
  ]
}

It is possible to also register context-aware parametric aspects in den.defaults. This is how you can provide a default to all hosts or users that match a condition.

# This example is split to aid reading.
let
  hasOneUser = host:
    length (attrNames host.users) == 1;
  hasUser = host: user:
    hasAttr user.name host.users;
  makeAdmin = user: {
    nixos.users.users.${user.name} = {
      extraGroups = [ "wheel" ]; 
    };
  };

  # IFF host has ONE user, make it admin
  single-user-is-admin = { host, user }: 
    if hasOneUser host && hasUser host user
    then makeAdmin user else { };
in
{
  den.default.includes = [
    # will be called *ONLY* on when the
    # `{ host, user }` context is used.
    single-user-is-admin
  ];
}

Custom parametric providers.#

The following is the code for how den.default is defined.

{ den, ... }: {
  den.default.__functor = den.lib.parametric true;
}

You can do the very same for other aspects of you that can have context-aware aspects in their .includes.

{ den, ...}: {
  den.aspects.gaming.__functor = den.lib.parametric true;

  # any other file can register gaming aspects
  den.aspects.gaming.include = [
    ({ emulation }: {
      nixos = ...;
      includes = [ den.aspects.steam ];
    })

    { nixos = ...; } # always included
    ({ gpu }: { }) # non-match on my-laptop
  ];

  # then you can depend on the gaming aspect:
  den.aspects.my-laptop.includes = [ 
    (den.aspects.gaming { emulation = "neogeo"; })
  ];

}

For more examples on parametric aspects explore our batteries.

Use the source, Luke!

Aspect dependencies#

Accessing an aspect module causes flake-aspects to resolve it by including the aspect's own class module and the same-class module of all its transitive includes.

Aditional to this, den registers some special dependencies designed to aid on Den particular use case: Host/Users, Homes.

Host dependencies#

Host also include den.aspects.<host> { inherit host; } meaning all included parametric aspects have an opportunity to produce aditional configurations for the host.

Also for each user, den.aspects.<user> { inherit host user; } is called. So den.aspects.alice.nixos can provide config to all hosts where alice lives. And also has the opportunity to register parametric aspects on alice.provides that inspect the host attribute to contionally produce other aspects.

User dependencies#

User modules are read from os-home configurations.

It basically invokes den.aspects.<user> { inherit host user; } but this time the host also contributes back generic configs to the users. If you are wondering how this is not recursive, the answer is by using our contexts, user-to-host dependencies start with a { host } context, while host-to-user dependencies start with a { user, host } context, and even when both are given to den.aspects.<user> ctx the results are non recursive.

A user also depends on den.default { inherit host user; }.

Home dependencies#

Home just uses its own module, its includes, and invokes den.default { inherit home; }.

Aspect Organization Patterns#

Learn about organizing patterns for reuse.

No two nix configurations are the same. We all tend to organize things as we feel better. This section will try to outline some hints on possible ways to organize aspects, none of this is mandatory, and indeed you are encouraged to explore and share your own patterns and insights.

Having a namespace of aspects.#

The first principle is using .provides. (aka ._.) to nest your aspects as they make sense for you.

Unlike normal flake-parts, where modules are flat and people tend to use names like flake.modules.nixos."host/my-laptop" or nixos."feature/gaming" to avoid collission, in den you have a proper tree structure.

I (vic), use an aspect vix for all features on my system, and from there I create sub-aspects.

Because writing den.aspects.vix._.gaming._.emulation tends to be repetitive, I use the following vix alias as module argument.

This pattern is also shown in the default template, under _profile.

NOTE: den provides an angle brackets experimental feature that allows even shorter syntax for deep .provide. access. See import-non-dendritic.nix for an example usage.

# modules/namespace.nix
{ config, ... }:
{
  den.aspects.vix.provides = { };
  _module.args.vix = # up to provides
    config.den.aspects.vix.provides;
}
# modules/my-laptop.nix
{ vix, ... }:
{
  vix.gaming = {
    _.steam.includes = [ 
      vix.gpu 
      vix.performance-profile 
    ];
  };
}

See real-life example from vic/vix

Using parametric aspects to route configurations.#

The following example routes configurations into the vix namespace. This is just an example of using parametric aspects to depend on other aspects in any part of the tree.

# modules/routes.nix
{ den, ... }:
let
  noop = _: { };

  by-platform-config = { host }:
    vix.${host.system} or noop;

  user-provides-host-config = { user, host }:
    vix.${user.aspect}._.${host.aspect} or noop;

  host-provides-user-config = { user, host }:
    vix.${host.aspect}._.${user.aspect} or noop;

  route = locator: { user, host }@ctx: 
    (locator ctx) ctx;
in 
{
  den.aspects.routes.__functor = 
    den.lib.parametric true;
  den.aspects.routes.includes = 
    map route [
      user-provides-host-config
      host-provides-user-config
      by-platform-config
    ];
}
{
  # for all darwin hardware
  vix.aarch64-darwin = { host }: {
    darwin = ...; 
  };

  # config bound to vic user 
  # on my-laptop host.
  vix.vic.provides.my-laptop = 
    { host, user }: {
      nixos = ...;
    };
}

Use your imagination, come up with an awesome Dendritic setup that suits you.

You made it to the end!, thanks for reading to this point. I hope you enjoy using den as much as I have done writing it and have put dedication on it for being high quality-nix for you. <3. I feel like den is now feature complete, and it will not likely change.

Contributing.#

Yes, please, anything!. From fixing my bad english and typos, to sharing your ideas and experience with den in our discussion forums. Feel free to participate and be nice with everyone.

PRs are more than welcome, the CI runs some checks that verify nothing (known) is broken. Any new feature needs a test in _example/ci.nix.

If you need to run the test suite locally:

$ nix flake check ./checkmate --override-input target .
$ cd templates/default && nix flake check --override-input den ../..