Your home in Nix (dotfile management)
- 1673 words
- 9 min
Updated 2021/02/02: I have now moved to a new system I'm calling Elemental. Expect an explanation post soon. You can find the Elemental repo at gh:hugoreeves/elemental. This post now serves as an account of how I previously managed my dotfiles with Nix.
Anyone who spends a significant amount of time in the terminal or developing applications on more than one device has come across the challenge of portable user configuration management.
The files used to configure tools like your shell, fish in my case, and your editor, neovim, are commonly referred to as 'dotfiles' and usually live within ~/.config/
Everyone tends to have their own unique solution to the problem; throughout most of 2018 and the start of 2019 I used the git bare repo approach described in this Atlassian Doc.
This solution served me well for a while, I could use a single repo per machine to store my dotfiles which enabled all the benefits of version control.
The big issue was that I had to keep my Darwin and Arch/NixOS configuration separate because many configs needed to be tailored to the individual system.
This made it particularly hard to keep common configs like my Neovim config in sync across Darwin and Linux.
It's safe to say that managing dotfiles is a tough problem and I wasn't happy with my git bare repo solution. After recently getting started with and learning to love Nix/NixOS, installing NixOS on my desktop and Nix on my Darwin machine, I had a renewed interest in improving my dotfile management. NixOS allows you to define your system configuration, system users, desktop environments, bootloader, etc, using the declarative Nix language and enables atomic updates that can be reliably rolled back. Using this ability I was able to successfully create a portable system configuration, that could be deployed using shared elements such as the Desktop Environment, to multiple computers running NixOS. Now, I wanted to do the same for my dotfiles.
Defining my problem
Ideally we want a single repository that enables sharing configuration and installed programs across any *nix operating system that supports nix. We must cater to an installation characterized as such.
- Multiple independent hardware devices. Refered to as machines.
- Multiple "desktop" environments, Darwin (MacOS), bspwm, kde, etc. Lets call these roles.
- Multiple users with specific package requirements. Referred to as users.
Defining a solution
Packages and settings should be installed/set depending on the selected, machine, role and user. Each of these three components, machine, role and user, can select the packages they require and override shared settings. After cloning the nixpkgs repository, a single file should be edited to compose the hardware, role and user.
Finding the right tools
When searching for dotfile management using nix, the number one tool that appears is Rycee's Home Manager.
This tool allows you to define settings for supported programs and services using nix expressions in a way comparable to NixOS configuration files.
For example, you can install some user packages and configure your .gitconfig
using a nix expression like this.
{ config, pkgs, ... }:
{
home.packages = with pkgs; [
# Rust CLI Tools! I love rust.
exa
bat
tokei
xsv
fd
# Development
neovim
tmux
jq
git-crypt
# Files
zstd
restic
# Media
youtube-dl
imagemagick
# Overview
htop
wtf
lazygit
neofetch
# Jokes
fortune
figlet
lolcat
];
programs.git = {
enable = true;
userEmail = "user@email.com";
userName = "user";
signing.key = "GPG-KEY-ID";
signing.signByDefault = true;
};
}
There's good documentation of all the options you can use within Home Manager. Now it's time to implement our solution.
Implementing a solution
Let's start with an entry point, ~/.config/nixpkgs/home.nix
, this is the file that home-manager uses by default as it's entry point.
# ~/.config/nixpkgs/home.nix
{ config, pkgs, ... }:
{
# Let Home Manager install and manage itself.
programs.home-manager.enable = true;
imports = [
./machine/apollo.nix
./role/darwin-laptop/index.nix
./user/x.nix
];
}
I have actually symlinked ~/.config/nixpkgs
to ~/nix-home
just to make it easier to cd
into the repo from a new shell.
Machine Configuration
There isn't much required in terms of machine level configuration, so this file will be left more or less empty.
# ~/.config/nixpkgs/machine/apollo.nix
{ config, lib, pkgs, ... }:
{
nixpkgs.config.allowUnfree = true;
}
Role Configuration
The role configuration contains config that is specific to the GUI/DE of the system.
I use Alacritty as my terminal, because it's a GUI app I want to place it's configuration in the role configuration. Here, I'm creating the role configuration ~/.config/nixpkgs/role/darwin-laptop/index.nix
.
Home Manager has built in support for Alacritty, this means you can define your ~/.config/alacritty/alacritty.yml
using a nix expression. A simple version of our role configuration, only setting Alacritty settings might look something like this.
# ~/.config/nixpkgs/role/darwin-laptop/index.nix
{ config, lib, pkgs, ... }:
{
programs.alacritty = {
enable = true;
settings = {
font.size = 11;
shell.program = "/usr/local/bin/fish";
};
};
}
Here, the settings will overwrite the default alacritty settings.
If your setup can be defined using only one role, ie. you only operate one distinct desktop configuration, defining program settings in your role file might work. However, we want to make our dotfiles portable, ideally we should have a shared set of Alacritty settings that can be imported into each role for which we want to use Alacritty, each role file should also be able to override some shared settings where necessary.
To achieve this, we'll create a file ~/.config/nixpkgs/program/terminal/alacritty/default-settings.nix
and populate it our Alacritty configuration.
# ~/.config/nixpkgs/program/terminal/alacritty/default-settings.nix
{
env = {
"TERM" = "xterm-256color";
};
background_opacity = 0.95;
window = {
padding.x = 10;
padding.y = 10;
decorations = "buttonless";
};
font = {
size = 12.0;
use_thin_strokes = true;
normal.family = "FuraCode Nerd Font";
bold.family = "FuraCode Nerd Font";
italic.family = "FuraCode Nerd Font";
};
cursor.style = "Beam";
shell = {
program = "fish";
args = [
"-C"
"neofetch"
];
};
colors = {
# Default colors
primary = {
background = "0x1b182c";
foreground = "0xcbe3e7";
};
# Normal colors
normal = {
black = "0x100e23";
red = "0xff8080";
green = "0x95ffa4";
yellow = "0xffe9aa";
blue = "0x91ddff";
magenta = "0xc991e1";
cyan = "0xaaffe4";
white = "0xcbe3e7";
};
# Bright colors
bright = {
black = "0x565575";
red = "0xff5458";
green = "0x62d196";
yellow = "0xffb378";
blue = "0x65b2ff";
magenta = "0x906cff";
cyan = "0x63f2f1";
white = "0xa6b3cc";
};
};
}
Now, we can import the shared alacritty settings into our role file. We'll also overwrite some of the settings where neccesary. lib.attrsets.recursiveUpdate
performs a deep merge of arg1
with arg2
overwriting values.
# ~/.config/nixpkgs/role/darwin-laptop/index.nix
{ config, lib, pkgs, ... }:
{
nixpkgs.config.allowUnfree = true;
programs.alacritty = {
enable = true;
settings = lib.attrsets.recursiveUpdate (import ../../program/terminal/alacritty/default-settings.nix) {
shell.program = "/usr/local/bin/fish";
};
};
home.file.".hammerspoon".source = ../../de/darwin-only/hammerspoon;
}
You may have noticed this last statement in the file home.file.".hammerspoon".source = ../../de/darwin-only/hammerspoon;
This statement sources (copies file/s) from the local directory path ../../de/darwin-only/hammerspoon
into the home directory path ~/.hammerspoon
This is the primary method of sharing config files that cannot be configured using home manager. Hammerspoon is a utility app that I use on Darwin that allows binding custom keyboard shortcuts to lua scripts.
User Configuration
User configuration stores common elements for your userspace that are platform independent.
I store my git configuration, and cli tools in the user configuration. The two imports, neovim/default.nix
and tmux/default.nix
are just nix expressions that source my ~/.config/neovim/
and ~/.config/tmux/tmux.conf
files. The .vimrc
files for neovim are stored under ~/.config/nixpkgs/program/editor/neovim/config-files/*
.
Whenever I want to edit my neovim configuration I have to edit a file like ~/.config/nixpkgs/program/editor/neovim/config-files/keybinds.vimrc
and then run home-manager switch
to have the files sourced into the ~/.config/neovim
folder. This may sound like a pain, having to run a command to deploy your changes each time you make them, but for me the advantages of having a single repo for cross os/de config management outweigh this slight pain point.
# ~/.config/nixpkgs/user/x.nix
{ config, pkgs, ... }:
{
imports = [
../program/editor/neovim/default.nix
../program/terminal/tmux/default.nix
];
home.packages = with pkgs; [
# Rust CLI Tools! I love rust.
exa
bat
tokei
xsv
fd
# Development
neovim
tmux
jq
git-crypt
dnsutils
whois
# Files
zstd
restic
brig
ipfs
# Media
youtube-dl
imagemagick
# Overview
htop
wtf
lazygit
neofetch
# Jokes
fortune
figlet
lolcat
];
programs.git = {
enable = true;
userEmail = "hugolreeves@gmail.com";
userName = "Hugo Reeves";
signing.key = "738A0BE6D8D8AE7D";
signing.signByDefault = true;
};
}
Setting up a new system
Now, my dotfiles have been properly split up and modularized so that I can compose a deployment of dotfiles and packages on any unix distribution with the ability to override shared configuration depending on the platform.
On a new system with nix installed, say, my NixOS machine, I can simply clone my nix-home repository into ~/.config/nixpkgs
and create a new home.nix
file. Within this file I can import the user/x.nix
config I use on Darwin and create a new role, role/bspwm-workstation/index.nix
, and machine, machine/boron.nix
, config. Within the workstation file, I can import the shared Alacritty configuration and overwrite some of the settings where necessary.
# ~/.config/nixpkgs/role/bspwm-workstation/index.nix
{ config, lib, pkgs, attrsets, ... }:
{
home.packages = with pkgs; [
dunst
compton
];
services = {
polybar = {
enable = true;
config = ../../de/bars/polybar/boron-config;
script = "polybar top &";
};
};
programs.alacritty = {
enable = true;
settings = lib.attrsets.recursiveUpdate (import ../../program/terminal/alacritty/default-settings.nix) {
font.size = 11;
font.user_thin_strokes = false;
window = {
decorations = "full";
};
};
};
xdg.configFile = {
"dunst/dunstrc".source = ../../de/notifications/dunst/dunstrc;
"compton/compton.conf".source = ../../de/compositors/compton/boron-compton.conf;
};
}
Summary
I think I'm finally happy.
Nix and home-manager make it possible to store all of my dotfiles in a single repo and on a single branch.
Separating my dependency path into the machine, role, user, system allows host specific adjustments to the a configuration.
Now, if I update my neovim config on one system, and check the changes into github, on another system I can just run cd ~/.config/nixpkgs; git pull; home-manager switch
I encourage you to checkout my nix-home repo if this system interests you.