How I manage my dotfiles
This post's featured URL for sharing metadata is https://www.jvt.me/img/profile.jpg.
Over the last month or so, there have been a couple of posts around how to manage local configuration (aka "dotfiles") - Better Dotfiles (discussion on Hacker News) and Managing dotfiles with chezmoi (discussion on Lobsters) - and it's made me reflect on my own usage.
Instead of replying on those discussion threads, I thought I'd take a second to write something a little bit more in-depth, a la IndieWeb or blogumentation.
(Unfortunately this has ended up taking about a month to actually sit down and finish, so it was a much more timely post, but its my blog and I'll blog at the pace I want! I've also apparently meant to blog about this for at least 4 years π)
I've been managing my dotfiles in their current form since December 2014 (!), and have basically changed nothing since then, which is pretty cool to think that this has worked across at least a dozen distinct machines, and various fresh installs of OSes, on personal devices and work devices, and a mix of Linux and Mac OSX.
Or maybe it just shows that I'm quite averse to change π
(I feel like this blog post is a little ADHD-like in the tangents and flow, so please bear with!)
Overall directory structure
The best place to start is to show you what my dotfiles-arch repo looks like.
As the name suggests, this follows my move onto Arch Linux, which also means I've been running that for a decade now π
At the top-level of my repository, I have the following:
bootstrap.sh
unpack.sh
bspwm/
dunst/
firefox/
go/
gpg/
java/
kitty/
nvim/
pkgbuilds/
sxhkd/
systemd/
terminal/
vim/
The directories try and group related sets of dotfiles - for instance my java
folder, as the name suggests, takes all my Java-related setup, like Gradle, Maven and IntelliJ configuration, as well as installing any dependencies needed for working in the JVM ecosystems.
In a lot of cases, I don't need everything to be unpacked + installed, as I don't often use all of the same tools and tech on all my devices.
For instance, in jobs where I've been on a Mac, I won't try and set up bspwm
or sxhkd
, as they're related to my Linux-based tiling window manager setup.
On every machine - including long-lived machines over SSH - I'll have terminal
and nvim
set up for the very key parts of my workflow, as I severely struggle without the muscle memory and saved keystrokes of my shell commands, or Neovim how I like it.
Within each group of packages, there is a hierarchy that allows me to split some key concerns about a set of dotfiles.
For instance, let's look at how my terminal
setup looks:
terminal
βββ bootstrap.sh
βββ dependencies
βββ global
βΒ Β βββ etc
βΒ Β βββ tmpfiles.d
βΒ Β βββ wksp.conf
βββ home
βΒ Β βββ bin
βΒ Β βΒ Β βββ c
β β ...
βΒ Β βΒ Β βββ ytoj
βΒ Β βββ .dir_colors
βΒ Β βββ .gitconfig
β β ...
βΒ Β βββ .Xresources
βΒ Β βββ .Xresources.MajesticMoose.local
βΒ Β βββ .zprofile
βΒ Β βββ .zshrc.local
βββ README.md
Notice that:
- There is a distinction between files that should be global (when I'm on Linux) and which should purely be in my home directory
- The
global
directory matches the directory structure from the root file system, soterminal/global/etc/tmpfiles.d/wksp.conf
should be put into/etc/tmpfiles.d/wksp.conf
- The
home
directory matches the directory structure from my home directory, soterminal/home/bin/c
should be put into$HOME/bin/c
- Some filenames, such as
.Xresources
also have a "local" counterpart like.Xresources.MajesticMoose.local
, which indicates that it should only be unpacked when running on a device with the hostnameMajesticMoose
- The
dependencies
file includes packages that need to be installed - There may be a
bootstrap.sh
, which will provide any arbitrary installs or processing i.e. for mygo
setup, I'llgo install
a number of binaries
Symlinks
The central concept - which is mundane for dotfiles management - is that I've got everything primarily managed through symbolic links (symlinks) as they make it fairly straightforward to manage things.
The first iteration of the project, in December 2014, used GNU Stow as a way to manage these directories and their setup.
I then removed it almost immediately removed in Jan 2015, largely to increase portability, but also as a way to play around with understanding how I'd do it myself. I seem to remember there were some additional features it had that I didn't really need to use, so thought to remove the "bloat".
Creation of symlinks is managed through a top-level script, unpack.sh
, which can be invoked i.e.
# unpack all of the dotfiles in the `terminal` folder
./unpack.sh terminal
I don't (yet?) have a way to clean up dangling symlinks.
Bootstrapping + installing things
As noted above, we have two means for installing packages or for performing any other required bootstrap.
For instance, nvim/dependencies
looks like so:
REPOS=(neovim xdotool lua python python-pynvim fzf vscode-json-languageserver gopls efm-langserver shellcheck \
vscode-html-languageserver yaml-language-server)
AUR=(ruby-solargraph typescript-language-server vim-language-server)
In this, we'll install a number of packages from the Arch repositories - via pacman
- and then install a number of Arch User Repository (AUR) packages - via yay
.
This is managed through a top-level script, bootstrap.sh
which triggers the installs, and then calls unpack.sh
on that directory.
As I find new packages I need, I'll add them to this list. Then on other machines, I'll run the following to get back in sync:
git pull && ./bootstrap.sh {directories that have changed}
What about directories?
As above, the directories themselves used to be created as symlinks themselves.
But as I started to do more things with systemd, I found that the systemd unit files themselves couldn't be symlinked, and as noted in 2016, I found I needed to symlink the parent directory.
Device-specific
There are a number of configs that are specific to the device I'm running on, so shouldn't be run on every device.
For instance on my desktop I want a slightly different monitor layout than I'll have on my single laptop screen, or my work laptop is regularly wired up to an external monitor, but can also be used on its own.
To manage these device-specific configs, I've ended up managing this through unpacking dotfiles that are named based on the hostname they're running on.
Therefore I'll have i.e. the following files:
terminal/home/.Xresources
terminal/home/.Xresources.MajesticMoose.local
This will then get expanded to:
/home/jamie/.Xresources -> /home/jamie/dotfiles-arch/terminal/home/.Xresources
/home/jamie/.Xresources.local -> /home/jamie/dotfiles-arch/terminal/home/.Xresources.MajesticMoose.local
In this case, I'll take advantage of Xresources' ability to include an external file.
This only works because I use a predictable naming scheme - based on Monty Python characters - and I know which of my devices are which.
I'll then also have a number of device-specific requirements on my work machine, which I won't push to my personal Git repo.
If I do want to commit them for backup or sharing purposes, I'll end up pushing a very lightweight version of a dotfiles repo on my work's source control platform.
Keep directory structure for ease
One thing I like about this setup is that it's very clear what the top-level directories are for, and then within there, it's the exact layout as it would sit on a filesystem.
Looking back at the history, it appears that this was a concept from stow
that I continued using, whereas I had misremembered it as me designing it myself.
Cross-platform
Early on I started feeling the pain of needing to work across multiple platforms, when I was working at Capital One and my daily driver was a Mac, but my personal devices were (and still are) all Arch Linux.
I think this was even why I removed the dependency on stow
, but my Git history unfortunately wasn't as good as I prefer it now.
By having to work cross-platform this has definitely helped me consider portability a little more, relying on tools that I know work the same way across OSes, albeit I'm a bad POSIX citizen so that makes it easier.
No other tools needed than yay
and plain shell script
As mentioned above, one driver to remove stow
was to simplify + make a more cross-platform setup.
But it was also because (I think) I felt I didn't need the dependency of another too..
The idea is that my Arch install will have been boostrapped - via the Installation Guide - and I'll make sure I have installed base-devel
.
Once I'd then manually built + installed yay-bin
, I'd be off to go.
Once I'd i.e. run ./bootstrap.sh
, I was then able to reboot, and logging in would β¨ just work β¨
And you know what? That's how it works, and has done, time and time again!
Conflicts and git stash
One of the fun things about this is that it ends up being rather git stash
heavy, and sometimes I have to deal with conflicts, which can be a pain. To my knowledge, I've not lost anything, at least.
When does something get promoted to my dotfiles?
In that note, how often do I take something and commit it up to be used by other devices? And how often should I do that? Well, "it depends"?
Sometimes I'll sit with a piece of configuration, for instance a Neovim plugin, for some time before promoting it to my defaults, if at all.
Sometimes it'll be so clearly useful that it needs to be committed + shared with my other devices. For instance a recent keybinding Super+Shift+d to spin up a local floating window with my local SQLite + Neovim workflow plugged into data from dependency-management-data got promoted as soon as I got it working, and it's been such a boost for the ad-hoc queries I run many times a day at work.
Happy?
I really am, especially looking back at how little change has been needed over the years. It's low-power, not the nicest code, but it's worked well for me, and keeps on working.
And it's wild to think that it's already been 10 years! Almost certainly the project that I've contributed to for the longest.