Dealing with Dots

Thursday, Jan 14, 2021
#unix #rice #tips

What follows is a brief description of the techniques I currently use to organize my personal configuration files, often called “dot files.” In general, most of your configurations are stored in a folder in your home directory called .config. This directory can be changed, but in my opinion it’s a sensible default.

In theory, every program you configure in Linux/UNIX will store its config files in .config, making it very easy to migrate your configs to new computers or track that folder with a version control system like git. Tracking your configurations with git allows you to easily clone the repository onto all your computers and keep your configs synchronized across them. You can browse your git history to look at any old versions of your configs and use branches to maintain different versions of some configs. For example, my desktop doesn’t need a battery indicator, but my laptop does.

Unfortunately, some popular programs don’t support this standard either because of incompetence, ignorance, or the program is just older than the XDG standard and can’t be updated due to legacy reasons. This includes vim (but not neovim), git itself, and most shells. Additionally, you might track more than just configurations such as an icon pack or a few fonts which would instead be (correctly) stored under .local/share.

The solution is to use symbolic links (or symlinks for short). They are one of the two types of links on UNIX and function similarly to “shortcuts” on Windows. A symlink is a regular text file that contains the name of another file or directory and has a file system flag of l. This is what indicates to programs that it is a symlink rather than a regular file. The standard way to create one is with ln -s.

$ touch /tmp/a
$ ln -s /tmp/a /tmp/b
$ echo "hello world" > /tmp/a
$ cat /tmp/b
hello world

In this example /tmp/b is a symlink that points to /tmp/a.

Whenever a program attempts to open a symlink it will instead seamlessly open the file that the symlink is pointing towards. Using this, we can store our configuration files anywhere we like and create symlinks in the “expected” places that point to our real files. Personally, I organize all my configs into a folder called dots with each program having its own subdirectory – like below.

$ tree -a ~/dots
dots
├── beets
│   └── .config
│       └── beets
│           └── config.yaml
├── dunst
│   └── .config
│       └── dunst
│           └── dunstrc
├── git
│   └── .gitconfig
├── i3
│   ├── .config
│   │   └── i3
│   │       └── config
│   └── .xinitrc
├── user-dirs
│   └── .config
│       ├── user-dirs.dirs
│       └── user-dirs.locale
└── zsh
    ├── .local
    │   └── share
    │       └── zsh
    │           └── plugins
    │               └── kota-prompt
    │                   ├── .git
    │                   ├── kota-prompt.plugin.zsh
    │                   ├── kota-prompt.zsh
    │                   ├── LICENSE
    │                   └── README.md
    └── .zshrc

Most have their own .config folder. Everything is arranged so that the contents of these folders can be symlinked to my home directory. The file at /home/kota/.config/i3/config is actually a symlink that points to /home/kota/dots/i3/.config/i3/config. It’s a little bit confusing, but this allows me to easily track of all of my dot files with git. A little bit of tooling makes it quite easy to manage.

Stow

stow [ options ] package ...

Stow is a fairly popular and old unix tool that is used to “manage farms of symbolic links,” which is exactly what we’re dealing with. It was originally created to help with manual software installs (and excels at that), but it also happens to be an amazing tool for managing your dot files.

stow deals with “packages,” which are directories arranged like the ones in my dots folder from above (zsh, user-dirs, i3 are all stow “packages”). It has options for “installing,” “removing,” or “updating” these packages. Installing a package simply means creating a bunch of symlinks in a certain location that point to the corresponding files in the package. I think the easiest way to explain this is with an example.

This is how I install my i3 configs on a new computer.

cd ~/dots
stow -t /home/kota i3

The -t option tells stow which folder all the symlinks are relative towards, or rather, where to actually install the package. Basically, if running cp -r i3/* /home/kota/ would copy all the files to the correct location, then /home/kota is the correct directory for the -t option.

stow defaults to creating symlinks, but if one or more of the directories exists it works around them. For example, if .config/i3 doesn’t exist stow will make a symlink for that folder that points to the one in your i3 package. However, if that folder does exist, stow will create a symlink for .config/i3/config instead.

Git

I’m going to assume you know the basics of git (add, commit, push, pull, clone), but if you haven’t used it much or need a refresher take a look at the first few chapters of the book.

There are 2 distinct “issues” you might run into with your new git tracked, stow managed, dots folder. I addressed the first earlier. What happens if you want to have slightly differing configs between your computers? You might want to use a different colorscheme on one computer, or not show a battery indicator on your desktop. Whatever the case, there are 3 ways of handling it.

The most obvious is using git branches. This approach is pretty clean and easy to understand, but there is one annoying downside: if you update a config on one branch, most of the time you’ll want to update it on all your branches. So, this approach involves a lot of remembering to merge and keeping all your branches up to date. If you have like 6+ computers this quickly becomes a huge pain in the ass, especially if they use almost the exact same configs for everything.

The second approach, which is even worse than the first (but requires slightly less git knowledge), is to have multiple repos, one for each computer. I did this for a while, but it has all the downsides of the first approach with more manual merges and none of the upsides.

The last approach, which I use currently, is having one repo and one branch, but different packages for the differing software. For example, if you have a sway config for your laptop and a different one for your desktop, you would have two folders sway-laptop and sway-desktop. You still need to remember to copy over changes that you want to make to both configs, but seeing them both physically in the directory makes it easier to remember. Plus, for all the software that doesn’t have differing configs, you only store one copy.

git submodule

The second distinct problem that comes up is how to handle “plugins” inside your configs. Specifically, I’m thinking of vim and zsh plugins, but most other software (except browsers) works similarly.

For zsh I made a little prompt plugin. To use it, you simply clone that repo to ~/.local/share/zsh/plugins/ or something similar and add a line like this to your zsh config source /home/kota/.local/share/zsh/plugins/kota-prompt/kota-prompt.zsh

However, now that you’ve added that line to your zsh config, you need to re-install that plugin whenever you setup a new computer. You could “solve” this by adding .local/share/zsh to your stow package and tracking the whole thing with git. That works well enough for small packages, but for bigger ones it really bloats your repo and now you need to manually update your plugins. Let’s be honest you’re not gonna remember to do that.

git actually comes to the rescue here with a lovely command called git submodule. This command allows you to specify other repositories as dependencies for your repository. For example, git submodule add https://github.com/tpope/vim-surround neovim/.config/nvim/pack/plugins/start/vim-surround is the command I ran from inside my ~/dots repo to install Tim Popes lovely vim-surround plugin. It will clone that repository into the location you specified, and now you can use the other git submodule commands to update or remove your plugins. You also need to make a commit that adds the submodule – similar to when you update a .gitignore. The git book covers that command in detail in chapter 7.11.

One thing to note is that now, when you clone your dots onto a new computer, you need to use the --recursive argument with git clone. For me, the command is git clone --recursive https://git.sr.ht/~kota/dots. When you want to update your plugins you can run git submodule update --remote --merge.