Modularizing my NixOS configuration
Recently there have been new Wayland desktop tools that have gained a lot of popularity. Namely the Wayland compositor Niri, and desktop-shells made with Quickshell such as Noctalia and DMS.
Given that I wanted to try these quite fundamental pieces to my desktop experience, but didn’t want to fully commit to them, I wanted to introduce some configurability to my Nix experience. With this I wouldn’t have to git revert or anything like that.
So how to introduce confirgurability into my system specifically? For this I should first give some overview of what my system looked like before. Here is a simplified view:
system-config/
├─ hosts/
│ ├─ tachyon/
│ │ ├─ default.nix
├─ modules/
│ ├─ eww/
│ │ ├─ default.nix
│ ├─ mako/
│ │ ├─ default.nix
│ ├─ hyprland/
│ │ ├─ default.nix
│ ├─ default.nix
├─ home/
│ ├─ eww/
│ │ ├─ default.nix
│ ├─ mako/
│ │ ├─ default.nix
│ ├─ hyprland/
│ │ ├─ default.nix
│ ├─ default.nix
│ ├─ index.js
├─ flake.nix
├─ flake.lock
├─ readme.org
I import the all the different modules of the different parts of my system, such as my compositor Hyprland, my bar eww and my notification daemon mako.
Now what I want to do is introduce a single configuration option that allows me to switch my bar from eww to noctalia-shell, however, some parts of my system are tied to the specific bar I chose to use.
For example, I have Hyprland keybinds related to my bar:
# === Start-up applications ===
exec-once=swaybg -m fill -i /home/phonon/Pictures/Wallpapers/gruv/houseonthesideofalake.jpg
exec-once = eww daemon
exec-once = eww open bar
Furthermore, Noctalia is closer to a fully integrated desktop environment rather than a simple bar. It includes a notification daemon, lockscreen, wallpaper daemon, volume & brightness control, and much more.
It replaces many other programs that I use to make my desktop environment work. So these also need to all be selectively imported.
In order to make this work we need to do a couple of things:
- Make an option in
flake.nixto select which bar to use - Import other programs specific to each bar inside that specific bar’s nix file.
- Make a Hyprland specific configuration per bar and import intelligently
To start, we can make an option in my flake.nix:
{
description = "Automatic system configuration using flakes for my laptops";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11";
...
noctalia = {
url = "github:noctalia-dev/noctalia-shell";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = inputs@{ self, nixpkgs, ..., noctalia }:
let
...
userSettings = {
wm = "hyprland"; # hyprland / niri
bar = "noctalia"; # noctalia / eww
terminal = "kitty"; # kitty / alacritty
};
in {
nixosConfigurations = {
tachyon = lib.nixosSystem {
inherit system;
inherit pkgs;
specialArgs = {
inherit inputs;
inherit pkgs-unstable;
inherit userSettings;
};
modules = [
./hosts/tachyon
./modules
./modules/desktop
home-manager.nixosModules.home-manager
{
...
home-manager.users.phonon = {
home.stateVersion = "25.05";
imports = [ ./home ./home/desktop ]
++ (if (userSettings.bar == "noctalia") then
[ inputs.noctalia.homeModules.default ]
else
[ ]);
};
home-manager.extraSpecialArgs = { inherit userSettings; };
}
];
};
};
};
}
Note the ellipses, for the full flake.nix visit my repo.
What we’ve done in the flake above is make some new configuration option, namely userSettings.bar. We make sure to inherit these as specialArgs, and in home-manager as extraSpecialArgs.
We can use these later to selectively import certain nix files.
We can also use this option to conditionally import the noctalia flake in home-manager. This can be done by adding to the import list with the ++ syntax and a simple if-else.
We can access these userSettings by importing them in a nix file, and then we can conveniently import the right file with some string concatenation:
{ inputs, config, lib, pkgs, userSettings, ... }:
{
imports = [
(../. + ("/" + userSettings.wm))
(../. + ("/bar-" + userSettings.bar))
(../. + ("/" + userSettings.terminal))
../xdg
../gruvbox
../librewolf
../rofi
../vimiv
../yazi
../zathura
];
...
}
To then handle the specific program configuration for different bars, i.e. using eww bar requires using programs such as mako, swaybg, hyprlock, we simply move those imports to the eww configuration:
{ inputs, config, pkgs, lib, ... }:
imports = [ ../mako ../hypr ];
...
}
The last thing we have to do to make this work now is make a Hyprland specific configuration for each bar. Luckily, this is pretty simple by just defining a home-manager option, which I already do for my system specific monitor configuration:
# Monitor configuration
home-manager.users.phonon.phononModules = {
hyprland = {
monitorConfig = ''
monitor=eDP-1,2256x1504, 0x0, 1.33
monitor=DP-1,1920x1080,auto-up,1
monitor=DP-3,1920x1080,auto-up,1
monitor=,highres,auto,1
bindl=,switch:on:Lid Switch,exec, hypr-screendisable open
bindl=,switch:off:Lid Switch,exec, $lockAndSuspendCmd && hypr-screendisable close
'';
};
};
Which is can then use in my Hyprland config as such:
{ inputs, config, pkgs, lib, modulesPath, pkgs-unstable, userSettings, ... }:
with lib;
let cfg = config.phononModules.hyprland;
in {
options.phononModules.hyprland = {
monitorConfig = mkOption { type = types.str; };
};
config = {
# Enable hyprland
wayland.windowManager.hyprland = {
enable = true;
xwayland.enable = true;
extraConfig = ''
$mainMod = SUPER
${cfg.monitorConfig}
I simply add a similar option like this in my eww configuration and use it in the Hyprland configuration.
And that’s basically it. Combining these tools allows one to untangle their configuration and create a lot more modularity.
A lot of this post was inspired by the great videos of LibrePhoenix, which go into a lot more detail. Tremendous thanks for the work in teaching in such a clear and understandable manner.