Program a Modular Control Center for Your Config Using Special Args in NixOS Flakes

Associated Youtube Video: https://youtu.be/H_Qct7TVB6o

Associated Odysee Video: https://odysee.com/@LibrePhoenix:8/program-a-modular-control-center-for:7

Previous Tutorial: NixOS Conditional Config and Custom Options

Next Tutorial: Using Both Stable and Unstable Packages on NixOS (At the Same Time!)

Tools Needed

So What Are specialArgs?

specialArgs (and extraSpecialArgs for home-manager) allow you to pass extra arguments to your NixOS module files. As a reminder, in a Nix module, the top part are the arguments:

{ config, lib, pkgs, ... }: # <- These are the arguments
{
  # stuff...
}

So, if you declare specialArgs or extraSpecialArgs, you can add arbitrary arguments:

{ config, lib, pkgs, some, arbitrary, arguments, yay, ... }: # <- These are the arguments
{
  # stuff...
}

These extra arguments can be anything, but for simplicity, I just import variables as specialArgs. For example, I could declare the username and name variables in my flake, and then import it into all relevant Nix modules:

# configuration.nix
{ config, lib, pkgs, username, name, ... }:
{
  # ...

  users.users.${username} = {
    isNormalUser = true;
    description = name;
    extraGroups = [ "networkmanager" "wheel" ];
    packages = [];
    uid = 1000;
  };

  # ...
}
# home.nix
{ config, lib, pkgs, username, ... }:
{
  # ...

  home.username = username;
  home.homeDirectory = "/home/"+username;

  # ...
}

The advantage to this is that you can have your configuration spread amongst many module files, but keep any variables that may need quick changes in one place.

How does this work?

Start by going to your flake.nix and navigating to the outputs section. The outputs should have a nixosConfigurations declaration (along with a homeConfigurations declaration if you’re running home-manager). The simplest format of this will look something like this:

{
  description = "My first flake!";

  inputs = { ... };

  outputs = { self, nixpkgs, ... }:
    let
      lib = nixpkgs.lib;
    in {
      nixosConfigurations = {
        YOURHOSTNAME = lib.nixosSystem {
          system = "x86_64-linux";
          modules = [ ./configuration.nix ];
      };
    };
  };
}

To add specialArgs, simply add them as an argument to the lib.nixosSystem function like so:

{
  description = "My first flake!";

  inputs = { ... };

  outputs = { self, nixpkgs, ... }:
    let
      lib = nixpkgs.lib;
    in {
      nixosConfigurations = {
        YOURHOSTNAME = lib.nixosSystem {
          system = "x86_64-linux";
          modules = [ ./configuration.nix ];
          specialArgs = {
            username = "librephoenix";
            name = "Emmet";
          };
      };
    };
  };
}

If you want to use this with home-manager, it’s the exact same idea, but instead they are called extraSpecialArgs:

{
  description = "My first flake!";

  inputs = { ... };

  outputs = { self, nixpkgs, home-manager, ... }:
    let
      lib = nixpkgs.lib;
      pkgs = nixpkgs.legacyPackages.${"x86_64-linux"};
    in {
      nixosConfigurations = {
        YOURHOSTNAME = lib.nixosSystem {
          system = "x86_64-linux";
          modules = [ ./configuration.nix ];
          specialArgs = {
            username = "librephoenix";
            name = "Emmet";
          };
        };
      };
      homeConfigurations = {
        USERNAME = home-manager.lib.homeManagerConfiguration {
          inherit pkgs;
          modules = [ ./home.nix ];
          extraSpecialArgs = {
            username = "librephoenix";
            name = "Emmet";
          };
        };
      };
    };
}

This will work, but it is of course more efficient to declare these extra arguments as variables in the let binding, and then just inherit them:

{
  description = "My first flake!";

  inputs = { ... };

  outputs = { self, nixpkgs, home-manager, ... }:
    let
      lib = nixpkgs.lib;
      pkgs = nixpkgs.legacyPackages.${"x86_64-linux"};
      username = "librephoenix";
      name = "Emmet";
    in {
      nixosConfigurations = {
        YOURHOSTNAME = lib.nixosSystem {
          system = "x86_64-linux";
          modules = [ ./configuration.nix ];
          specialArgs = {
            inherit username;
            inherit name;
          };
        };
      };
      homeConfigurations = {
        USERNAME = home-manager.lib.homeManagerConfiguration {
          inherit pkgs;
          modules = [ ./home.nix ];
          extraSpecialArgs = {
            inherit username;
            inherit name;
          };
        };
      };
    };
}

Now that you have them setup in specialArgs (or extraSpecialArgs), they can be utilized in any module file referenced by your configuration, simply by adding them to the argument list:

# configuration.nix
{ config, lib, pkgs, username, name, ... }:
{
  # ...

  users.users.${username} = {
    isNormalUser = true;
    description = name;
    extraGroups = [ "networkmanager" "wheel" ];
    packages = [];
    uid = 1000;
  };

  # ...
}
# home.nix
{ config, lib, pkgs, username, ... }:
{
  # ...

  home.username = username;
  home.homeDirectory = "/home/"+username;

  # ...
}

Depending on your config, you may have very unique use cases for this. In my config for example, I have a theme variable, which sets the theme of my entire system using Stylix. I also have the editor and browser variables to set my default applications across window managers and desktop environments. I even have the font variable, which, after some tinkering, is able to change the font used by (nearly) every app on my system!

Now I have 20+ variables! That’s too many arguments!

I agree! Having 20+ arguments to a single function is a bit unwieldy. I had this problem for the longest time until I realized that there was a very simple fix for this. Instead of declaring 20+ variables individually (which must be individually imported into each module as a separate argument), you can declare just a few attribute sets (with all the necessary information).

In my config, I only use two attribute sets like this, systemSettings and userSettings, like so:

outputs =
  let
    # ---- SYSTEM SETTINGS ---- #
    systemSettings = {
      system = "x86_64-linux"; # system arch
      hostname = "snowfire"; # hostname
      profile = "personal"; # select a profile defined from my profiles directory
      timezone = "America/Chicago"; # select timezone
      locale = "en_US.UTF-8"; # select locale
    };

    # ----- USER SETTINGS ----- #
    userSettings = rec {
      username = "emmet"; # username
      name = "Emmet"; # name/identifier
      email = "emmet@librephoenix.com"; # email (used for certain configurations)
      dotfilesDir = "~/.dotfiles"; # absolute path of the local repo
      theme = "uwunicorn-yt"; # selcted theme from my themes directory (./themes/)
      wm = "hyprland"; # Selected window manager or desktop environment; must select one in both ./user/wm/ and ./system/wm/
      # window manager type (hyprland or x11) translator
      wmType = if (wm == "hyprland") then "wayland" else "x11";
      browser = "qutebrowser"; # Default browser; must select one from ./user/app/browser/
      defaultRoamDir = "Personal.p"; # Default org roam directory relative to ~/Org
      term = "alacritty"; # Default terminal command;
      font = "Intel One Mono"; # Selected font
      fontPkg = pkgs.intel-one-mono; # Font package
      editor = "emacsclient"; # Default editor;
      # editor spawning translator
      # generates a command that can be used to spawn editor inside a gui
      # EDITOR and TERM session variables must be set in home.nix or other module
      # I set the session variable SPAWNEDITOR to this in my home.nix for convenience
      spawnEditor = if (editor == "emacsclient") then "emacsclient -c -a 'emacs'"
                    else (if ((editor == "vim") || (editor == "nvim") || (editor == "nano")) then "exec " + term + " -e " + editor else editor);
    };

  in
  {
    nixosConfigurations = {
      # ...
    }
    homeConfigurations = {
      # ...
    }
  }

If you switch to using attribute sets, but some of the components in those attribute sets are calculated using other parts of the attribuet set, it won’t work by default. The spawnEditor part of my userSettings attribute set displays this. In order to fix this, simply add rec (meaning recursive) to the start of your attribute set:

# ...

normalAttributeSet = {
  a = 1;
  b = a + 1; # this doesn't work
};

recursiveAttributeSet = rec {
  a = 1;
  b = a + 1; # works now!
}

# ...

Hope this was helpful!

I hope I have helped you on your journey towards NixOS enlightenment. Stay tuned for more!

Donation Links

If you have found my work to be helpful, please consider donating via one of the following links. Thank you very much!