Tom Butler's programming blog

Evict Your Darlings: Banish dotfiles from your home directory

Let me preface this by saying this an experiment, while I've been running it for a week without issue, YMMV and we are in here be dragons territory.

The title is a play on Graham Christensen's excellent article Erase your darlings which is an ingenious way to create a particualarly clean hard drive and generally tidy user experience. But we can improve upon it.

I want to take the set up a little further. All my config files are generated every boot by NixOS and placed in /home/tom. The .config directory, .local, .cache etc are annoying darlings that sit in my home directory making it look untidy. They never get touched directly by me, I don't need them there and they get deleted every reboot. I want them out. Shoo!

Spring cleaning

Because of the way NixOS works those dotfiles don't actually exist in my home directory. They're ghosts created to appease tradition and make the applications that rely on them existing in that location able to find their configuration.

I want to spring clean my home directory. It should contain only those photos, documents, programming projects and all that stuff I have. You know, the files I actually chose to create on my computer. I want to be able to move that whole directory to another computer without inadvertently copying all my games because steam chooses to install them into ~/.local. Even with impermanence there are things like the aforementioned .local/share/steam directory that will need to be persistent across boots that live in the home directory.

Anything that I didn't directly create or save into my home directory is clutter and needs to be banished.

The What

Here's the plan. Replace a familiar directory structure that looks like this:

/home/tom - .config - alacritty - ... - .ssh - .mozilla - .thunderbird - .steam - .zshrc - .zprofile - .local - Documents - Projects - Work - Downloads

With this idealistic directory structure:

/users/tom - home - Documents - Projects - Work - Downloads - config - local - alacritty - thunderbird - ... [everything else previously in ~/.config] - ssh - steam - .mozilla - zsh - zshrc - zprofile

In reality as I'll get to shortly some of the directories inside /users/tom/config will still have to have a dot prefix due to limitations of the applications that use them but at least they're banished from the home directory.

Cleanliness

With this set up in place the following files can be persistent using the impermanence module and everything else can be created on boot!

- config/.mozilla - config/.thunderbird - config/.local/share/steam - home/

Importantly, the home folder /users/tom/home can be entirely backed up without worrying about accidentally including firefox profiles, 12gb of emails from thunderbird, or steam games in the backup! It also feels incredibly clean, therefore god adjacent.

Considerations

There are several practical considerations here:

  • The "Home" button baked into innumerable UIs should still navigate to the profile home directory /users/tom/home
  • Open/Save dialogs should default to the relevant place (documents, videos, etc) or the profile home directory /users/tom/home
  • No UI should ever default to the /users/tom directory or navigate to it when pressing a home button

The How

The high level steps to execute this plan are:

  1. Set the user's $HOME to /users/tom/home
  2. Configure, convince or otherwise violently force applications to read/write their configuration files in /users/tom/config.

Actually achieving this goal requires a bit of work.

In a world where everything used XDG's config directory this would be simple and actually setting XDG_CONFIG_HOME moves .config and keeps it out of /home. Anything else needs handling on a case by case basis.

In reality, we need some jiggery-pokery to make this work. A lot of applications look for a hardcoded $HOME/.dotfile. Well, $HOME is a variable. We can change the location at any point or even set it differently for different applications.

To accomplish this spring clean we'll:

  1. Default $HOME to /users/tom
  2. After login but before starting the desktop environment we'll set $HOME to /users/tom/home
  3. Explicitly set $HOME to /users/tom/config before launching any naughty application that doesn't let us choose a different dotfile location

Basic NixOS user config

By having a two-tier home, services and anything that runs on login sees /users/tom as $HOME then anything that runs within the context of the desktop environment will have $HOME set to /users/tom/home.

Step 1 is setting the home directory to /users/tom and ensuring that /users/tom/home exists with the right permissions:

users.users.${globals.user} = { home = "/users/${globals.user}"; # This is required until this is merged: # https://github.com/NixOS/nixpkgs/pull/324618 # Reasoning in the PR homeMode = "0755"; createHome = true; }; # Use systemd.tmpfiles to create the home directory in the user's profile systemd.tmpfiles.settings = { "10-create-home" = { "/users/${globals.user}/home" = { d = { group = "root"; mode = "0700"; user = "${globals.user}"; }; }; }; };

My config has a variable set in ${globals.user} containing my username. I'll use this througout my examples but you can just substiute your username here if you don't have a variable for it.

Changing $HOME after login

The next step is to set $HOME to /users/tom/home after login but before the desktop environment is started.

How to do this will be different depending on your display manager and desktop environment. I'm using the tty text login prompt and launching Hyprland from the command prompt on login

ZSH is a bit of a pain to conifgure as although there is a ZDOTDIR environment variable and a programs.zsh.dotDir option in home-manager to set it, home-manager verifies if it's outside of the $HOME directory and uses $HOME in its generated config. Here's how I got around that and handle my early log in:

# Needed to make it the user's default shell programs.zsh.enable = true; # Set user's shell to ZSH so it is loaded on login users.users.${globals.user}.shell = pkgs.zsh; home-manager.users.${globals.user} = { programs.zsh = { dotDir = "config/zsh"; enable = true; plugins = [ { name = "powerlevel10k"; src = pkgs.zsh-powerlevel10k; file = "share/zsh-powerlevel10k/powerlevel10k.zsh-theme"; } { name = "powerlevel10k-config"; src = ./p10k-config; file = "p10k.zsh"; } ]; envExtra = '' # Run first in .zshenv # Forces ZDOTDIR and HOME so that the plugins can be loaded export ZDOTDIR="/users/${globals.user}/config/zsh" export HOME="/users/${globals.user}" ''; initExtra = '' # Runs after plugins have been loaded in .zshrc # Set $HOME after plugins have been loaded export HOME="/users/${globals.user}/home" # Additional custom config bindkey "^[[3~" delete-char if [ -z "$DISPLAY" ]; then # Navigate to ~ to run hyprland with $HOME CWD cd ~ hyprland fi ''; }; };

The clever part is that we set export HOME="/users/${globals.user}/home" immediately before launching the desktop environment. You'll need to do the same in your display manager if you're not using the text login prompt like I am.

Since 90% or more GUI applications sensibly use XDG_CONFIG_HOME and equivalents for .cache, .local, etc we can just happily set those in our config and remove the standard XDG .config, .local, etc out of our new $HOME:

home-manager.users.${globals.user} = { xdg = { enable = true; configHome = "/users/${globals.user}/config"; cacheHome = "/users/${globals.user}/config/cache"; dataHome = "/users/${globals.user}/config/local/share"; stateHome = "/users/${globals.user}/config/local/state"; }; };

Applications which ignore XDG constants

That's pretty much it! However, there are a few problematic dotfiles remaining which need to be handled on a case by case basis. Here's a few example fixes.

Git/SSH

My .ssh directory is actually created from my config and the only thing I really need it for is git. Move the files into config and use the GIT_SSH_COMMAND environment variable to point it to the relevant key:

home-manager.users.${globals.user} = { home.file."config/ssh/id_rsa".source = ./id_rsa; home.file."config/ssh/id_rsa".source = ./id_rsa.pub; home.sessionVariables = { GIT_SSH_COMMAND = "ssh -o IdentitiesOnly=yes -i /users/${globals.user}/config/ssh/id_rsa"; }; };

Firefox

Firefox always looks for a $HOME/.mozilla directory. Home has to be overriden:

{ pkgs, globals, ... }: let firefox = pkgs.firefox.overrideAttrs (a: { buildCommand = a.buildCommand + '' wrapProgram "$executablePath" \ --set 'HOME' '/users/${globals.user}/config' ''; }); in { home-manager.users.${globals.user} = { programs.firefox = { enable = true; package = firefox; }; }; }

The downside of this is that open/save dialogs default to the config directory and pressing "Home" in them takes you to config. Annyoing but I rarely use those dialogs in Firefox.

Steam

The steam package is a bit weird and doesn't support wrapProgram like firefox but still supports overriding the HOME variable like so:

programs.steam.enable = true; programs.steam.package = pkgs.steam.override { extraEnv = { HOME = "/users/${globals.user}/config"; }; }; The note about open/save dialogs in the firefox section above applies.

Closing thoughts

It feels nice having these ghosts removed from the home directory. While some applications require a little extra configuration, NixOS makes it fairly straightforward and allows us to banish dotfiles from our sight.

They're still there lurking in the config directory.

Should you do this?

Your mileage may vary but I've found it incredibly clean. Changing the HOME variable may cause weird side effects I haven't encountered.

Could you do the same on other distros?

It's almost certainly possible. You could take the same approach of changing the $HOME variable after login. However, would you want to? On other distros you'd want to back up most of those dotfiles alongside your documents. On Nix that config is all generated on boot.