Why should I write my config files in the Nix language?

For many programs, one can write the config files in the standard language of the program, or they can use the Nix language.

For example, when configuring hyprland, one can use the standard configuration language in a hyprland.conf file like so,

monitor = DP-1, 1920x1080, 0x0, 1

And just have home manager put that file in the correct spot.

Or one can use the Nix language, in a .nix file like this:

monitor = [
   "DP-1, 1920x1080, 0x0, 1"
];

I struggle to understand the benefit of using Nix instead of the standard configuration language for each program, and just letting home manager put the files where they need to go.

I supposed one benefit would be the ability to override certain sections of a configuration file for different computers (laptop vs desktop, etc).

So the question is, why should I configure hyprland (or any program) in Nix and not its standard configuration language?

4 Likes

There are probably other reasons, but here is my favorite.

Here’s an example flake:

❯  cat flake.nix
{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs";
  };
  outputs = { self, nixpkgs }: {
    packages.x86_64-linux.default = nixpkgs.legacyPackages.x86_64-linux.awk;
    packages.x86_64-linux.default = nixpkgs.legacyPackages.x86_64-linux.sed;
  };
}

Notice that I’m setting the default package to both sed and awk. If you try to build it:

❯  nix build
error: attribute 'packages.x86_64-linux.default' already defined at /nix/store/a8jzismmx028lx1igywd1g2xq0zw3lvn-source/flake.nix:6:5

       at /nix/store/a8jzismmx028lx1igywd1g2xq0zw3lvn-source/flake.nix:7:5:

            6|     packages.x86_64-linux.default = nixpkgs.legacyPackages.x86_64-linux.awk;
            7|     packages.x86_64-linux.default = nixpkgs.legacyPackages.x86_64-linux.sed;
             |     ^
            8|   };

Having multiple sources of truth for the same thing is an error in nix. Elsewhere, a program may take its configuration from one of a variety of config files. The Apache webserver for example…

On most systems if you installed Apache with a package manager, or it came preinstalled, the Apache configuration file is located in one of these locations:

  • /etc/apache2/httpd.conf
  • /etc/apache2/apache2.conf
  • /etc/httpd/httpd.conf
  • /etc/httpd/conf/httpd.conf

You could have two of these and waste a bunch of time editing whichever one doesn’t have precedence and wondering why your edits don’t make a difference. I know I’ve wasted a lot of time being wrong about where a certain config was coming from. Errors, on the other hand, those you can fix almost immediately.

If there’s no risk of this kind of thing in your case, I think it’s perfectly fine to just use home manager to plop config files where the program expects them.

If you later need to update the contents of those files based on some parameter that is known to nix, you can use pkgs.substituteAll to inject the parameter. I do this here. I want wezterm to use nushell and zellij, so I inject their /nix/store paths into wezterm.lua.

Like Apache, wezterm has a similarly complex way of deciding which config file to use. So my config would break if I tried to use it on a machine which had $XDG_CONFIG_HOME set to something weird. But since I never do that it’s fine to just plop the file.

I guess the moral of the story is that conventional file locations are a bit of a mess, and it’s a mess that nix tries to clean up.

5 Likes

Enables things such as GitHub - catppuccin/nix: ❄️ Soothing pastel theme for Nix

2 Likes

Thank you both for your response. I will say that I am still not convinced, and I find it much easier to just use the language and syntax provided by the program and using a line like:

  home.file.".config/my-config-file".source = ./myconfigfile.config;

In my home manager config. Otherwise I am trying to figure out the translation between the standard configuration language, and the nix language, which has caused headaches for me in the past.

1 Like

Should is a hard question to answer. You could use nix if you wanted to do things like generate the config on the fly, like merging different optional sets together before writing the final file. Using nix for the configuration can also help when the definition for the configuration covers all the options and their types. This can flag something like misspelling monitor as momitor before restarting with the new configuration.

But you don’t need to do this and you don’t have to if you don’t want to.

8 Likes

Merging different optional sets together is the best reason I could think of and definitely a valid reason. Thanks for the insight.

This is a perfectly good way to do things as well. For me, which I use in a given case depends on

  • how much I like the application’s native config format
  • how well I know that config format (if I don’t know it well, I’d definitely rather use Nix since I actually know Nix pretty well)
  • whether I already have much configuration written in said config format
  • how deeply the Nix module integrates with that config format (in your example, it seems not very-- you’ve essentially got a list there that is stringly typed in the Nix code)
  • how much I want to programmatically generate that configuration (i.e., whether and how much that program’s configuration varies across my systems)

I think it’s natural that sometimes you’ll like to use a Nix module to generate your configuration file entirely, and other times you’ll prefer to reuse a native config file. Neither method is right or wrong; just do what feels comfy and natural for you and feel free to reevaluate as your preferences and use cases evolve over time.

9 Likes

I do a mix of the two. Simpler applications where I don’t need third-party maintenance, templating or convenient access to loops stay in the domain-specific config language because there are more docs and I don’t really need nix features. For me that’s stuff like eww or screen.

More complex things are configured with nix, because folks in the home-manager repo figure out how to deal with things like systemd integration for me and ensure that it keeps working.

Sometimes I write the config files in the original language and use builtins.readFile to insert them into the home-manager options to get the best of both worlds. This requires some reading of the modules, but it’s how I handle e.g. sway and zsh.

An outlier is emacs, but I’ve put a lot of effort into integrating it properly. That’s only really possible for turing-complete config languages, though.

4 Likes

Super helpful as always TLATER, thank you. This is pretty much the approach that I plan to take going forward.

Emacs is for the exception where I made an unholy combination of the two approaches outlined here.

I provide all the packages and symlink the main configuration but that then calls code that I can adjust on the fly without having to rebuild.

For most other applications I would recommend a more declarative approach however.

1 Like

This is the exactly the main reason that I prefer writing my configs in nix instead of whatever the original config syntax is: I really really really don’t want to learn yet another config syntax, and simultaneously, nix is usually better than whatever the original config syntax was.

Trailing very close behind is the fact that it makes reusing constants between different configs extremely easy, e.g. if I want to set some address binding in hosts while also using that same binding somewhere else.

2 Likes

A NixOS-newbie here,

Is there a “nix-native” way to use config file templates? I am thinking of Jinja2 or similar. I also find it easiest to keep config files in the native format (easier to find documentation), but if there were a way to use templates, it could be reusable between several hosts/configurations. Just thinking “aloud”.

-J

1 Like

The Nixpkgs standard environment has a few substitution functions like substituteInPlace that can serve simple use cases. But of course you can generate config files (or any other files) using any template engine you want, using runCommand.

2 Likes

Consider also the fact that Nix is a programming language, which is more powerful than a templating language, so you can easily have “templates” in Nix:

let myParameter = "foo";
in
{
  home.file.".config/my-config-file".text = ''
    # My arbitrary config file format
    some-literal-config
    another-config-directive
    # Yay string interpolation
    ${if myParameter == "foo" then "enable-frobnicate" else "disable-frobnicate"}
  '';
}

You can reference any data inside conditions such as this

2 Likes

reusing constants is definitely a great use of Nix for a configuration language.

However, I disagree that using just Nix instead of the original syntax equals less learning. For me, I end up just having to learn the original syntax, plus the translation from the syntax to Nix, which ends up being even more work.

That gets easier as your nix-foo improves, so I agree that sometimes using a nice home-manager module is easier.

It depends a bit on how “rich” the underlying module is, and how complex the underlying config language is, though. Anything using JSON will be much better to configure directly with nix, for example, because there’s an easy 1:1 mapping, and you gain all the strengths of the module system. Your favorite programmable editor is a very different story.

Any of the approaches people have brought up will work, though, what’s easiest/best comes down to context, experience and a bit of preference. Easiest and best may not always align, either.

I’d say do what you like for personal projects, and don’t be afraid to reevaluate and rewrite stuff after some time - opinions can and should change with experience. Do whatever results in the lowest maintenance burden elsewhere, and again, reevaluate and rewrite if it’s not actually working down the line.

Either way, I don’t think there is a universal best practice to be found here. If we continue the thread this way it’s just going to be a list of different scenarios and empty arguing.

2 Likes

This should be an introduction to the official (still planned but not yet worked on) tutorial on declarative configurations.

1 Like