Quirk Recipes
These recipes share one shape. Many small aspects each contribute a piece of data under a quirk key, and one aspect consumes the combined list. The contributors never name the consumer, and the consumer never lists the contributors. What is included decides what gets collected.
Language aspects that configure every editor
Section titled “Language aspects that configure every editor”The problem
Section titled “The problem”A “Go” aspect wants three things: the Go toolchain on the user’s PATH, the Go
extension in the user’s editor, and Go-specific editor settings. The toolchain
is ordinary homeManager config. The editor parts are awkward:
- if the Go aspect writes
programs.vscode.*directly, it only works for VSCode, and a second editor (VSCodium, a VSCode fork) needs a second copy; - if the editor aspect lists every language, adding a language means editing the editor.
The recipe
Section titled “The recipe”Declare two quirks, one per kind of editor data:
den.quirks.editor-extensions.description = "Editor extension packages";den.quirks.editor-settings.description = "Editor settings attrsets";Each language aspect installs its toolchain and emits editor data, without knowing which editor will read it:
den.aspects.lang.go = { homeManager = { pkgs, ... }: { programs.go.enable = true; home.packages = [ pkgs.gopls pkgs.gotools ]; };
editor-extensions = { pkgs, ... }: [ pkgs.vscode-extensions.golang.go ];
editor-settings = [ { "go.useLanguageServer" = true; } ];};
den.aspects.lang.nix = { homeManager = { pkgs, ... }: { home.packages = [ pkgs.nixd pkgs.nixfmt ]; };
editor-extensions = { pkgs, ... }: [ pkgs.vscode-extensions.jnoortheen.nix-ide ];
editor-settings = { pkgs, lib, ... }: [ { "nix.enableLanguageServer" = true; "nix.serverPath" = lib.getExe pkgs.nixd; "[nix]"."editor.formatOnSave" = true; } ];};Settings that are not language-specific go in a shared core aspect the same way:
den.aspects.editor.core.editor-settings = [ { "editor.rulers" = [ 80 120 ]; "files.trimTrailingWhitespace" = true; "telemetry.telemetryLevel" = "off";} ];Each editor aspect consumes both quirks:
den.aspects.editor.vscode.homeManager = { editor-settings, editor-extensions, lib, ... }: { programs.vscode = { enable = true; profiles.default = { userSettings = lib.mkMerge editor-settings; extensions = lib.unique (lib.flatten editor-extensions); }; }; };A second editor aspect with the same body (programs.vscodium, or any other
fork) reads the same two lists. A role aspect then picks what a user gets:
den.aspects.roles.dev-gui.includes = with den.aspects; [ editor.core editor.vscode lang.go lang.nix];
den.aspects.tux.includes = [ den.aspects.roles.dev-gui ];flowchart LR
subgraph L["language aspects (producers)"]
GO["lang.go"]
NIX["lang.nix"]
CORE["editor.core"]
end
EXT[("editor-extensions")]
SET[("editor-settings")]
subgraph E["editor aspects (consumers)"]
VS["editor.vscode"]
AG["editor.vscodium"]
end
GO --> EXT & SET
NIX --> EXT & SET
CORE --> SET
EXT --> VS & AG
SET --> VS & AG
Every producer feeds every consumer through the two quirks, and neither side names the other.
Adding lang.rust to the role adds its toolchain, its extension and its
settings to every included editor. Adding an editor gives it every
included language. Neither edit touches the other side.
Why it works
Section titled “Why it works”{ pkgs, ... }:quirk values.pkgsis not scope context, so these values are config-dependent thunks. Den resolves them inside the consuming Home Manager evaluation, with that evaluation’spkgs. The extension list therefore uses the same package set (and overlays) as the rest of the home.lib.mkMerge editor-settings. Each list entry becomes one module definition ofuserSettings, so the module system merges them key by key. Home Manager typesuserSettingsas JSON, so lists (likeeditor.rulers) from several aspects are concatenated. Two aspects setting the same scalar key to different values is a conflict error, which is what you want. Uselib.mkDefaultin the core aspect for values a language may override.lib.unique (lib.flatten …). Each value is a list, so the pool is a list of lists.uniquedrops an extension that two languages both requested.
Every aspect declares what it persists
Section titled “Every aspect declares what it persists”The problem
Section titled “The problem”The list of paths to keep is the sum of what every enabled service needs:
/var/lib/docker for Docker, /etc/ssh for SSH host keys,
~/.config/VSCodium for the editor. Kept in one central list, it drifts:
removing an aspect leaves its paths behind, and adding one forgets them.
The recipe
Section titled “The recipe”Each aspect declares its own paths next to the config that needs them:
den.quirks.persist.description = "System paths to keep across reboots";den.quirks.persistHome.description = "Home paths to keep across reboots";
den.aspects.docker = { nixos.virtualisation.docker.enable = true; persist.directories = [ "/var/lib/docker" ];};
den.aspects.ssh = { nixos.services.openssh.enable = true; persist = { directories = [ "/etc/ssh" ]; files = [ "/etc/machine-id" ]; };};
den.aspects.editor.vscode.persistHome.directories = [ ".config/Code" ".vscode" ];One collector per level turns the pool into the impermanence option:
den.aspects.impermanence = { nixos = { persist, lib, ... }: { environment.persistence."/persist" = { directories = lib.unique (lib.concatMap (e: e.directories or [ ]) persist); files = lib.unique (lib.concatMap (e: e.files or [ ]) persist); }; }; homeManager = { persistHome, lib, ... }: { home.persistence."/persist" = { directories = lib.unique (lib.concatMap (e: e.directories or [ ]) persistHome); files = lib.unique (lib.concatMap (e: e.files or [ ]) persistHome); }; };};Include impermanence on the hosts (and users) that wipe their root. On a
host without it, the persist keys are still collected but nobody reads them,
so the same service aspects work on both kinds of host.
The same shape works for a second list with a different lifetime, such as a
cache quirk collected into a volume that is not backed up.
Overlays contributed by the aspects that need them
Section titled “Overlays contributed by the aspects that need them”The problem
Section titled “The problem”The recipe
Section titled “The recipe”Aspects contribute overlays to a quirk, and one consumer per class applies them:
den.quirks.nixpkgs-overlays.description = "nixpkgs overlays collected from aspects";
# wrapped in `_:` -- see the caution belowden.aspects.editor.core.nixpkgs-overlays = _: [ inputs.nix-vscode-extensions.overlays.default ];
den.aspects.nixpkgs-overlays = { nixos = { nixpkgs-overlays, lib, ... }: { nixpkgs.overlays = lib.unique nixpkgs-overlays; }; homeManager = { nixpkgs-overlays, lib, ... }: { nixpkgs.overlays = lib.unique nixpkgs-overlays; };};Put the consumer in den.default.includes so every host and home has one.
By default each home collects its own overlays. When a host shares its
package set, a policy exposes each user’s overlays up to the host, but only
on hosts where sharing is on:
den.policies.project-user-overlays = { user, host, ... }: lib.optionals host.shareHomePkgs [ (den.lib.policy.pipe.from "nixpkgs-overlays" [ den.lib.policy.pipe.expose ]) ];
den.schema.user.includes = [ den.policies.project-user-overlays ];host.shareHomePkgs here is a user-defined host option (see
Aspect Settings for a typed way to declare such
flags). The policy returns no effects on other hosts, so their users’ overlays
stay in their homes.
Designing your own
Section titled “Designing your own”The three recipes use the same checklist:
- Name the data, not the consumer.
editor-settings, notvscode-settings: the key describes what the producer knows. - Emit lists. Each producer emits a list and each consumer flattens, dedups or merges. Lists are what let many producers contribute.
- Merge in the consumer. The consumer chooses the merge rule
(
mkMerge,unique,concatMap). Producers stay plain data. - Use a policy only when data must move. Everything here is same-scope except the overlay expose. When data has to cross scopes, add a pipe and nothing else.
See also
Section titled “See also”- Quirks & Pipes guide — producing, consuming, scoping, thunks
- Cross-Scope Pipes —
expose,collect,broadcast - den.quirks reference — the pipe builder API
- Aspect Settings — typed per-host tunables
- Case Study: Kubernetes — quirks across hosts and a cluster