Skip to content

Context System

Use the Source, Luke: modules/context/types.nix · modules/context/os.nix

In Den, a context is an attribute set whose names (not values) determine which functions get called. When Den applies a context { host, user } to a function { host, ... }: ..., the function matches. A function { never }: ... does not match and is ignored.

Named contexts ctx.host { host } and ctx.hm-host { host } hold the same data, but hm-host guarantees that home-manager support was validated. This follows the parse-don’t-validate principle: you cannot obtain an hm-host context unless all detection criteria passed.

graph LR
  H["den.ctx.host {host}"] -->|"hm-detect"| Check{"host OS supported by HM?<br/>host has HM users?<br/>inputs.home-manager exists?"}
  Check -->|"all true"| HMH["same data {host}<br/>as den.ctx.hm-host"]
  Check -->|"any false"| Skip["∅ skipped"]
  HMH -->|"guaranteed"| Use["HM pipeline<br/>proceeds safely"]

Each context type is defined in den.ctx with four components:

den.ctx.foobar = {
desc = "The {foo, bar} context";
conf = { foo, bar }: den.aspects.${foo}._.${bar};
includes = [ /* parametric aspects */ ];
into = {
baz = { foo, bar }: [{ baz = computeBaz foo bar; }];
};
};
ComponentPurpose
descHuman-readable description
confGiven context values, locate the configuration aspect
includesParametric aspects activated for this context (aspect cutting-point)
intoTransformations fan-out to other context types

A context type is callable — it’s a functor:

aspect = den.ctx.foobar { foo = "hello"; bar = "world"; };

When applied, Den:

  1. Produces owned configs from the context type itself
  2. Locates the aspect via conf (e.g., den.aspects.hello._.world)
  3. Applies includes — parametric aspects matching this context
  4. Transforms — calls each into function, producing new contexts
  5. Recurses — applies each produced context through its own pipeline
graph TD
  Apply["ctx.foobar { foo, bar }"]
  Apply --> Own["Owned configs"]
  Apply --> Conf["conf → locate aspect"]
  Apply --> Inc["includes → parametric aspects"]
  Apply --> Into["into.baz → new contexts"]
  Into --> Next["ctx.baz { baz }"]
  Next --> Own2["...recurse"]

Transformations have the type source → [ target ] — they return a list. This enables two patterns:

graph TD
  subgraph "Fan-out (one → many)"
    Host1["{host}"] -->|"into.user"| U1["{host, user₁}"]
    Host1 -->|"into.user"| U2["{host, user₂}"]
    Host1 -->|"into.user"| U3["{host, user₃}"]
  end
  subgraph "Conditional (one → zero or one)"
    Host2["{host}"] -->|"into.hm-host"| Gate{"detection<br/>gate"}
    Gate -->|"passes"| HM["{host} as hm-host"]
    Gate -->|"fails"| Empty["∅ empty list"]
  end

Fan-out — one context producing many:

den.ctx.host.into.user = { host }:
map (user: { inherit host user; }) (attrValues host.users);

One host fans out to N user contexts.

Conditional propagation — zero or one:

den.ctx.host.into.hm-host = { host }:
lib.optional (isHmSupported host) { inherit host; };

If the condition fails, the list is empty and no hm-host context is created. The data is the same { host }, but the named context guarantees the validation passed.

Contexts are aspect-like themselves. They have owned configs and .includes:

den.ctx.hm-host.nixos.home-manager.useGlobalPkgs = true;
den.ctx.hm-host.includes = [
({ host, ... }: { nixos.home-manager.backupFileExtension = "bak"; })
];

This is like den.default.includes but scoped — it only activates for hosts with validated home-manager support. Use context includes to attach aspects to specific pipeline stages instead of the catch-all den.default.

Add new transformations to existing context types from any module:

den.ctx.hm-host.into.foo = { host }: [ { foo = host.name; } ];
den.ctx.foo.conf = { foo }: { funny.names = [ foo ]; };

The module system merges these definitions. You can extend the pipeline without modifying any built-in file.

Den defines these context types for its NixOS/Darwin/HM framework:

The entry point. Created when evaluating den.hosts.<system>.<name>:

den.ctx.host.conf = { host }:
parametric.fixedTo { inherit host; } den.aspects.${host.aspect};
den.ctx.host.into.default = lib.singleton;
den.ctx.host.into.user = { host }:
map (user: { inherit host user; }) (attrValues host.users);

Transforms into default (for global aspects) and user (for each user).

Created for each user on a host:

den.ctx.user.conf = { host, user }@ctx: {
includes = [
(fixedTo ctx userAspect)
(atLeast hostAspect ctx)
];
};
den.ctx.user.into.default = lib.singleton;

den.default is an alias for den.ctx.default. Every context type transforms into default via .into.default:

den.ctx.default.conf = _: { };

This is how den.default.includes functions receive their context data — host, user, or home contexts all flow through here.

Activates only for hosts with Home-Manager users. Imports the HM module. See Context Pipeline for detection criteria.

Created for each HM user, forwards homeManager class into the host.

Entry point for standalone Home-Manager configurations.

Create your own for domain-specific pipelines:

den.ctx.greeting.conf = { hello }:
{ funny.names = [ hello ]; };
den.ctx.greeting.into.shout = { hello }:
[{ shout = lib.toUpper hello; }];
den.ctx.shout.conf = { shout }:
{ funny.names = [ shout ]; };

Applying den.ctx.greeting { hello = "world"; } produces both "world" and "WORLD" through the transformation chain.

See the Context Pipeline for the complete data flow. See the den.ctx Reference for all built-in types.