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!)
Table of Contents
Tools Needed
- A system with the Nix Package Manager installed
- Your configuration managed by a
flake.nix
- Home-manager installed with
home.nix
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!