Since getting back into running NixOS, I have been having a whale of a time configuring my Linux machine. The declarative life is somewhat addictive. However, I felt that I could be improving my configuration in a couple of ways. First, while I was learning how to configure NixOS, I had tended to scatter bits of code about randomly, and so it started to become a challenge to find what I needed. Second, I was getting the itch to bring some of the benefits of declarative configuration to my macOS machines. You obviously have less control there (even with nix-darwin), but I have tailored my Emacs, Neovim and shell environment just the way I lik…
Since getting back into running NixOS, I have been having a whale of a time configuring my Linux machine. The declarative life is somewhat addictive. However, I felt that I could be improving my configuration in a couple of ways. First, while I was learning how to configure NixOS, I had tended to scatter bits of code about randomly, and so it started to become a challenge to find what I needed. Second, I was getting the itch to bring some of the benefits of declarative configuration to my macOS machines. You obviously have less control there (even with nix-darwin), but I have tailored my Emacs, Neovim and shell environment just the way I like them, and every time I use macOS, it irks me that I don’t have access to the nice configuration that is set up on my Linux box.
In my browsing around, I came across Denix, which solves both problems very nicely.
Denix, written by yunfachi, is a Nix library that makes it easy to structure a configuration in such a way that you can create portable modules for programs or services, or groups of these to provide ‘features’, and then set up hosts to enable the modules you need. Denix provides a bit of ‘syntactic sugar’ which makes writing modules with options quicker and easier, but essentially provides a wrapper around Nix’s native lib.mkOption. This is a good thing, because you could go back to using standard Nix options if Denix stopped being maintained.
However, the structure is very clever and helps you to think in a much more portable, modular way about your configuration code. Each module (whether for services or programs) can have three sections (each of which is optional) for NixOS, nix-darwin or home (for Home Manager). The brilliant thing about this is that it is very clear what your code is targeting, and you can set one program or service up in one place with the full configuration across hosts.
The structure adds host configuration in hosts/myhostname.nix files, and in these, you set up host-specific configuration (such as hardware) and then enable programs, services and features as required in one neat chunk of code. A desktop machine (like mine) might need all the bells and whistles for daily use, whereas a server might only enable the shell/CLI tools plus specific web server packages and so on. In practice, this makes it easy to read and understand what is enabled on each host, and once you have invested in configuring a module for something, it only takes one line of code to reuse it on another host. I have also found that it has encouraged me to create shell aliases or write custom scripts for a particular programs in the module where I install that program. Previously, I would have declared all my aliases and scripts with my shell configuration, but this makes more sense. You can see your whole configuration for the program in one place, and if you disable the module, none of the accompanying aliases, scripts and so on get installed. It’s a small thing really, but a great ‘quality of life’ improvement in a large configuration.
The final part is the rices/ folder. This is somewhere you can keep different themes and visual appearance styling, then define for each host which theme you want it to use by default. The themes are actually built as flake ‘specialisations’. I still don’t grasp this totally, but they are essentially versions of the full configuration that only differ in a minor way (in this case, the theme). This means that you can switch immediately to an alternative theme like so (where morse is my hostname on this box):
sudo nixos-rebuild switch --flake .#nixosConfigurations.morse-gruvboxLight
# then back again to the default dark gruvbox theme like this:
sudo nixos-rebuild switch --flake .#nixosConfigurations.morse
You can also define parts of themes that are for import to other themes only. I’ve done this to avoid repetition in setting fonts, font sizes, terminal opacity and so on that I typically want to be the same between themes. Then the main theme files import that and set colours, wallpapers and so on. I’m using Stylix which is a wonderful project that lets you set a particular base-16 colour scheme in one place, then apply it to everything. It’s a very easy way to get a coherent look and feel when using a window manager, and with the Denix rices, you can instantly (-ish) switch to another base-16 theme.
I wanted to set up a proper structure for my NixOS configuration first, but now that I have that in place, configuring my macOS machines should be relatively easy because most of the work is done. connerohnesorge has a well documented Denix configuration with some macOS hosts, so I will be learning (and probably copying code) from that when I get to that point. For the NixOS configuration, I learned a huge amount from compactcode’s configuration, and also copied whole modules from it as it was set up to do just what I wanted. Enormous thanks to both for making their repositories available — it really helps to have examples to learn from.
On that note, I have now made my repository available in the nixos repo on Codeberg, in case it is useful to anyone else. I needed to wait until I had figured out how to encrypt genuine secrets in the configuration (with SOPS) and hide confidential information (like private email addresses) using git-crypt. If you’re curious about how I did that, the information is in the README file.
I also put my purchased fonts in a private repository with its own flake, and then pull that in to my configuration to install and configure those fonts. I set up a separate repo for my nixvim configuration (i.e. neovim) (also on Codeberg), which again gets pulled in as an input to my Denix flake. I have found it a bit easier to keep it separate, and it enables me to run my fully configured neovim on any system with nix installed without actually installing it. I’m not sure how often I will actually be doing that, but it’s cool to know I can!
Finally, I have started using devenv for coding projects where I need to set up an environment with certain tools and their dependencies, which might differ between projects. It’s a brilliant project, and makes it so easy to set up a fully declarative coding environment per project, complete with scripts, automatic execution of stuff on shell enter and so on. It’s as easy as cd-ing into a project directory (or creating one), then doing devenv init which creates the configuration files you need. You then fill in what packages you want, write any scripts you need, and every time you change into that directory, you have access to what you need, without polluting your main configuration. It works extremely well, and means that you only need to learn one way to configure virtual environments across programming languages.
Obviously this is all still very much a work in progress, but I’m very happy with it so far. In case you were wondering, I have been trying to diversify my git forge activity a bit, and start to move away from GitHub as the main location for my code. Codeberg is a great community-driven project, and I am hoping to set up a regular donation to support their work. I will probably mirror my repositories to GitHub just for discoverability (once I have figured out how to do that), and also retain some private repositories on GitHub for now.