tty.is_

The Home Manager Journey

Home-manager is a narrowly-focused system for managing user environments with Nix. After setting it up, users under its management can have their own declarative configuration not only for what programs are available, but how those programs and services are configured.

The Home Manager Manual is quite good and should help you get it configured for your environment. I've personally configured it the flakes version of the NixOS module.

"But why?"

What if you wanted to have bash not record curl commands to history. You can do this in a session something like this:

export HISTIGNORE=$HISTIGNORE:curl

Of course this won't persist across sessions. You could take it a step further and drop this export command into your .profile, which will work but…

…what's that icky feeling? That gnawing sense that something is wrong? You've gotten your NixOS configuration just the way you like it1, and now there's this… this stuff you've got to do to get your environment right. There's an extra step (echo export HISTIGNORE=$HISTIGNORE:curl >> ~/.profile) to perform, or an extra file (~/.profile) you have to carry around with you.

The promise of the declarative config pilgrimage was that you could ascend to a higher plane of computation – one where clean installs are indistinguishable from machines that have been running for years. A place where scaling your configuration from a single desktop machine under your desk to HA VM clusters in the cloud is just a difference in how you invoke nixos-rebuild2.

Yet here you are – running nano ~/.profile for the fifth time today. A StackOverflow answer made flesh3.

A holy journey that doesn't challenge you, doesn't make you question your beliefs, is hardly worth taking. I surely have not finished my journey, and so do not know all that is in store for you. Indeed, you may have taken different paths to get to where you are, so I imagine there's much for us to learn from one another… or at least I may "borrow" some useful stuff from your Nix config.

This exchange only truly works for us if transaction is on equal terms. A random .profile checked into a public repo might as well be a curl-pipe-to-sudo-bash as far as I'm concerned. I refuse to touch it.

.profile and other dotfiles like it (e.g. .bashrc) are a bunch of opinions and spices tossed into a blender, then baked. If you don't like the whole dish as served, your options are:

  • Suck it up
  • Send it back
  • Try mixing in more opinions and spices, maybe pick out pieces you don't want, and just… hope for the best?

I don't want the dish you baked, I wanna know how you made it.

This is what I'm on about:

programs.bash.historyIgnore = [ "curl" ];

I can confidently pilfer the blob of Nix goo above and mix it into whatever I've got going on in my setup. It's under version control.

It's futureproof.

Today, bash consults the HISTIGNORE environment variable for a :-separated list of commands to leave out of history. If you're reading ahead, you may note that there are lots of other ways to get this behavior using Nix using environment/session variable facilities, but none of those are quite as well structured.

Imagine a meteor strikes Earth4, and bash stops honoring HISTIGNORE and looks somewhere else for its list of things to leave out of history, say some set command or something. In the old .profile-based world, you are really truly screwed. You get to dig around in your configuration to figure out why thing used to work no longer work. Worse yet, you figure it out, but have some systems still running bits from the old pre-meteor days. You now get to maintain two versions of your magic .profile: one that does the env var thing, and another that does the set thing.

But that's not you, my sibling in Nix. You have learned your lessons well. You used home-manager and configured this setting as above. The world does not change for you5. Your old systems chug along using the env var setup. Your new systems (referencing newer versions of home-manager), use the new set setup under the hood.

This is transparent to you.

You have ascended. Your journey continues.

But what about unsupported programs?

You may be surprised at just how many things are configurable in home-manager, but it's certainly not every single program, so you're eventually going to come across something for which no declarative configuration method is available. You'll have a dotfile in some format as the only real means of configuration.

I should buy a boat, you say to yourself. All of this hard work, and here I am back at the start.

So what are you going to do? scp the config from some other live system? Adopt GNU Stow as another tool you have to learn/run/reconcile with Nix?

Stay on the path.

Treat the configuration as an opaque file

You've been tempted by the worldly pleasures of a program called cursed (my apologies if there's already something out there with this name, as there surely is). For the purposes of our conversation, cursed is available in nixpkgs, but there aren't any NixOS or home-manager modules for it. To configure it, you must create a configuration file, ~/.cursedrc.

You can check your darling dotfile into your repo and get it sucked into your config and plopped down in your $HOME using home-manager:

[settings]
cursed = true
user = "thetraveler" # imagine this is your username

and your home-manager config:

home.file.".cursed-config" = {
  source = ./.cursed-config; # this is relative to this .nix file
  target = ".cursedrc";      # this is the destination relative to $HOME
};

This is… better than before. Your .cursedrc is under source control, and is placed on disk when you apply your home-manager config.

Treat the configuration as text in your config

Now that you've given yourself some breathing room by just Making Things Work, you can start bringing the opaque blob closer to your Nix config. Delete your darling dotfile.

home.file.".cursed-config" = {
  target = ".cursedrc";
  text = ''
    [settings]
    cursed = true
    user = "${config.home.username}"
  '';
};

This is almost the same as before, but notice that we have taken advantage of Nix to further decouple our configuration. If you change your username or apply this config to more than one user, you've made your journey a little easier.

Treat the configuration as config within your config

There's still something unsettling about our solution… It's just not very Nix-y… What if we wanted to make it easier to, say, add a new block to our config or something.

home.file.".cursed-config" = {
  target = ".cursedrc";
  source = (pkgs.formats.toml {}).generate "cursed-toml" {
    settings = {
      cursed = true;
      user = config.home.username;
    };
    newblock.whodis = "value";
  };
};

Note that we switched back to using source here… this is because the TOML generator emits output as a derivation (i.e. it doesn't spit out a string, it spits out a file):

[newblock]
whodis = "value"

[settings]
cursed = true
user = "myusername"

This is powerful. You can now use the Nix language to not only integrate your Nix configuration into cursed, but you can avoid having to edit configuration in a lesser format like TOML, JSON, or shudder YAML. You can find more supported formats here.

Treat the configuration as a call to action

If you're interested in bending cursed to your needs, other travelers on other paths may also have this interest. The next step here is to author a full-on module. I won't cover that in this post (perhaps in the future our paths will cross again), but there are plenty of good resources out there.

I'd probably start with the NixOS Manual, then look at the home-manager specific stuff here.

The advantages of a full on module are manifold, but the first and most obvious one is that you can enforce type restrictions. There's nothing we've talked about so far that will prevent you from, say, setting cursed to "me" or ["foo" "bar"] instead of true.

Footnotes:

1

I kid, we all know that your config is never done.

2

Well… that and the size of your cloud spend.

3

Damn, I'm feeling spicy today.

4

Oh no, my .bash_history!

5

Modulo meteor