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:
- Set the user's $HOME to
/users/tom/home
- 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:
- Default
$HOME
to/users/tom
- After login but before starting the desktop environment we'll set
$HOME
to/users/tom/home
- 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.