This project exists for personal reasons, but occasionally I'm asked how I configure my development environment. I use these dotfiles to quickly set up my environment and achieve feature parity on different Unix-like operating systems.
The developer community offers several dotfile managers and other great ideas for how to manage dotfiles. While I'm biased toward my strategy, feel free to ignore this if some other system is going to level up your productivity (xkcd #1205 outlines my favorite approach to measuring this).
Assuming I have a package manager installed (either Homebrew or a well-supported native option), the first tools I install are:
- GNU Stow
- Git (if necessary)
Stow answers the question of how to expose this configuration to various tools after cloning this repository. Familiarity with package managers and Git is assumed, but both have excellent documentation for anyone just getting started.
I chose Stow based on these requirements:
-
The tool should be easy to bootstrap on popular Unix-like operating systems
As a GNU tool, Stow is consistently available with package managers. It's also easy to install from source if necessary (it's just a Perl module). -
The learning curve for the tool should be shallow
Stow follows the Unix philosophy of doing one thing and doing it well, so it's easy to master. -
Configuration for the tool should be optional, not required
My Stow configuration is limited to ignore lists and resource files, both of which are optional. -
The tool should support managing dotfiles for multiple hosts and environments
I'll talk about how Stow can support this under Environment-Specific Configuration.
Stow was created to solve a problem unrelated to dotfile management:
Stow addresses the need to administer, upgrade, install, and remove files in independent software packages without confusing them with other files sharing the same file system space.
More specifically, it lets you install Perl into a directory like
/usr/local/stow/perl
and create symlinks that make it appear as if its files were
installed under these directories:
/usr/local
├── bin # Executable binaries
├── lib # Libraries
└── man # Man(ual) pages
This aligns to the Filesystem Hierarchy Standard and makes it easier for other tools
to find the Perl installation (e.g., man perlrun
).
If you wanted to uninstall a tool that was automatically installed in these directories,
you'd traditionally need to remove each file manually, assuming you knew which files
needed to be removed. You might've had access to a make
target that did this for you
if you were lucky. Stow, however, can remove its symbolic links and completely uninstall
Perl with a single command.
Today it is less common to install software this way, given the wide availability of mature package managers. Dotfile management is primarily about managing symlinks, however, and Stow handles this well.
Let's say we wanted to share a Git global ignore file across multiple development hosts. We'd first create an ignore file in the following directory structure below our home directory. Stow refers to some of these directories by logical names:
/Users/Andrew # The "target" directory
└── dotfiles # The "stow" directory
└── git # A "package" directory
└── .config
└── git
└── ignore
The physical names of the stow and package directories are flexible. However, the name of the stow directory will typically match the name of your Git repository, and I choose to organize package directories by the tools I'm configuring.
Next, we can use a single Stow command to expose this file to Git. The git
argument for
this command refers to the package directory and the -v
option allows us to see what
Stow is doing:
~/dotfiles $ stow -v git
LINK: .config/git => ../dotfiles/git/.config/git
You can see that Stow creates one symlink to mirror the contents of the git
package
directory within our target directory (two levels higher). This is exactly where Git
expects to find the global ignore file:
/Users/Andrew # The "target" directory
└── .config
└── git -> ../../dotfiles/git/.config/git
Stow is powerful enough to work with existing directories in the target directory (e.g.,
.config
) and even manages symlinks that overlap with other package directories.
This concept is referred to as tree folding, which you can read about in the
documentation.
Stow can create symlinks in any target directory using the -t
option, including
grandparent or sibling directories. This gives us the flexibility to locate our dotfiles
directory somewhere else in the file system, including directories within other stow
directories (more on this below).
This repository uses a few Git submodules and they need to be initialized after cloning:
~/dotfiles $ git submodule update --recursive --init
I also use a Stow resource file to implicitly set my target directory
for stow
commands, which makes it easier to work with nested stow directories. Before
running any other commands in this repository, I effectively set up Stow using Stow:
/Users/Andrew/dotfiles/local/home/macos $ stow -vt /Users/Andrew stow
LINK: .stowrc => dotfiles/local/home/macos/stow/.stowrc
If you're forking this repository, .stowrc
should be modified to refer to your target
directory.
The directory structure of this repository resembles the following:
.
├── tool-a #
├── tool-b # Multiple package directories
├── tool-c #
│
├── local
│ ├── home
│ │ ├── macos # A nested stow directory for MacOS packages
│ │ │ ├── tool-a
│ │ │ ├── tool-b
│ │ │ └── stow
│ │ └── ubuntu # A nested stow directory for Ubuntu packages
│ │ ├── tool-a
│ │ └── stow
│ └── work
│ └── centos # A nested stow directory for CentOS packages
│ ├── tool-a
│ └── stow
│
└── etc # An ignored directory for location-agnostic configuration
By using nested stow directories, I'm able to layer configurations based on my current environment or host. I prefer to use separate directories for my personal work and my day job, but any system can be used here.
Using Git as an example again, I maintain a global .gitconfig
file that I use in all of
my environments and a host-specific file that my .gitconfig
includes via the
include.path directive. I've named the latter gitconfig.inc
.
Git expects this global file to be located in the home directory, so I've added it to the Git package with the global ignore file that I introduced above:
. # The top-level stow directory
└── git # The package directory for git
├── .config
│ └── git
│ └── ignore # The global ignore file I introduced earlier
└── .gitconfig
The following command creates the appropriate link to my .gitconfig
, in addition to
creating a link to the directory containing the global ignore file:
~/dotfiles $ stow -v git
LINK: .gitconfig => ../dotfiles/git/.gitconfig
LINK: .config/git => ../dotfiles/git/.config/git
Next, gitconfig.inc
is located in a package under local/home/macos
, and I've chosen
to symlink it in .config/git
with Git's other configuration:
local/home/macos # The nested stow directory
└── git # A MacOS-specific package directory for git
└── .config
└── git
└── gitconfig.inc
To create the appropriate link to this file, I cd
to the nested stow directory and repeat the
stow
command above. Remember that a Stow resource file implicitly sets the target
directory to my home directory:
~/dotfiles/local/home/macos $ stow -v git
UNLINK: .config/git
MKDIR: .config/git
LINK: .config/git/ignore => ../../dotfiles/git/.config/git/ignore
LINK: .config/git/gitconfig.inc => ../../dotfiles/local/home/macos/git/.config/git/gitconfig.inc
The resulting directory tree will look like this:
/Users/Andrew # The "target" directory
└── .config
└── git
├── ignore -> ../../dotfiles/git/.config/git/ignore
└── gitconfig.inc -> ../../dotfiles/local/home/macos/git/.config/git/gitconfig.inc
You might notice that Stow replaces the .config/git
symlink created in the first step
with a directory, and then symlinks the individual files from both the top-level
package and the nested package. This is automatic when Stow recognizes the original
symlink, which is made possible by including an empty .stow
file in each stow directory
(see Configuring Stow below).
Local configuration can also be organized under a single package:
local/home # The nested stow directory
└── macos # A package directory for all configuraton used on MacOS
└── .config
├── git
└── zsh
This reduces the installation of local configuration to a single command:
~/dotfiles/local/home $ stow -v macos
It's also possible to write a script that simplifies the process of stowing local
packages. I set up new environments so infrequently, however, that I don't mind running
stow
for a small number of local packages.
Stow doesn't address the need to maintain private keys or other secrets. One option is to encrypt them manually with PGP, commit them, and then decrypt them when setting up a new host. However, this assumes that the PGP keys are manually copied to each host.
Stow's lack of support for encryption is a trade-off between simplicity and comprehensive functionality (see my requirements at the start of this README).
The following Stow-specific files are used in this repository:
-
./<package>/.stow-local-ignore
This ignore list is required when a package directory contains files that shouldn't be symlinked. It uses the Perl regular expression syntax.
-
./local/<stow-directory>/stow/.stowrc
This resource file makes it possible to define default command-line options (e.g., the target directory).
-
.stow
,./local/<stow-directory>/.stow
These empty files help Stow work with multiple stow directories.
The remainder of this README covers the software I use most and their corresponding
configuration files. I use brew
to install almost everything on both MacOS and Linux-based
hosts.
Note that my configuration is very opinionated and I've written most of it from scratch to fit my needs (with plenty of ideas borrowed from the community). While everything should just work, it's probably only useful as an example and I didn't write it to be copied as-is.
alacritty/alacritty
Alacritty is a fast terminal emulator. It's also very simple (to a fault, some might say),
so those who expect support for window tabs and other basic functionality may find it harder
to use.
tmux/tmux
Alacritty eschews tabs in favor of pairing the emulator with tmux, which multiplexes
a single terminal to simulate panes and windows. There's a learning curve for tmux, but
it allows me to use a consistent terminal environment across most emulators (along
with a few other nice features).
gnachman/iterm2
In the off-chance I'm unable to run Alacritty, I've backed up some iTerm2 configuration
that closely resembles my Alacritty settings.
tmux-plugins/tpm
TPM is the standard plugin manager for tmux. I currently use it for:
-
tmux-plugins/tmux-resurrect
This lets you recreate an entire Tmux session after a restart, including windows, current directories, and even Neovim sessions (see vim-obsession under Editor). -
christoomey/vim-tmux-navigator
This works together with a Neovim plugin to enable moving between Neovim and tmux windows with the same keyboard shortcuts. I'll talk more about this below.
zsh-users/zsh
Z Shell (Zsh) was a gateway drug of sorts for a generation of command-line users. It
provides a programming language like Bash with features like tab completion and
themeable prompts, transforming a command line into a powerful editor. The cool kids
might be using fish, but I like Zsh for its active development community and its
similarities to POSIX shells.
sorin-ionescu/prezto
One of the most popular projects on GitHub is a configuration and plugin manager called
Oh My Zsh (OMZ). I used OMZ for a few years before I ultimately dropped it for Prezto,
which was created after its author couldn't convince Robby Russell to simplify the
framework. While the Prezto community is significantly less active than OMZ's, my command
prompt loads more quickly with Prezto.
Usage | Global Script | Local Script |
---|---|---|
Environment variables for all shells | Not used | .zshenv |
Configuration for login shells (includes $path ) |
.zprofile | .config/zsh/zprofile.inc |
Configuration for interactive shells | .zshrc | .config/zsh/zshrc.inc |
Prezto configuration | .zpreztorc | Not used |
See the Prezto documentation and chapter 2 of the Zsh guide for an explanation of these files.
neovim/neovim
Neovim began as a fork of Vim in 2014 with a faster plugin system, a built-in terminal
emulator, and many other features. I'll only use Vim if I'm on a host where I can't
install Neovim, but since this happens often when I log in to remote servers, my Neovim
configuration is backwards-compatible with Vim. My configuration will
also apply Neovim's defaults in Vim so that the editing behavior is identical between the
two.
If you're viewing .vimrc
in Neovim, use zo
and zc
in normal mode to open and close
folds.
junegunn/vim-plug
vim-plug is one of several good options for plugin management with support for both Neovim
and Vim. My preferred plugins are installed in my .vimrc
, the most important of which are:
-
vim-airline/vim-airline
This provides visual cues and context-specific information in Neovim's status line. See Themes below. -
tpope/vim-obsession
This wraps Neovim's:mksession
to transparently save windows, tabs, and buffers. By pairing this with tmux-resurrect, you can automatically restore your editors when you restore your tmux session. -
christoomey/vim-tmux-navigator
As mentioned above, this works together with a set of tmux key bindings to enable moving between Neovim and tmux windows with the same keyboard shortcuts.
I use custom themes for tmux, Zsh, and Neovim, inspired by Powerline (a collection of status line plugins written in Python) and powerlevel10k (a Zsh theme). My theme for Neovim was created for the vim-airline plugin and my prompt theme was created for Prezto.
While Powerline and powerlevel10k are both powerful and extensible, I prefer not to use them for a few reasons:
-
Powerline didn't support Neovim at the time I switched from Vim. Powerline is also a heavyweight framework that requires background Python processes, and I've found vim-airline to be faster and more reliable.
-
powerlevel10k targets the Zsh prompt only. I prefer a minimal prompt theme, so the overhead of another large dependency doesn't seem necessary to me.
The themes above use glyphs (icons) available with Nerd Fonts, a collection of well-known fonts patched with glyphs from Powerline, FontAwesome, and other icon-based fonts. I use JetBrains Mono, however, any font from the collection should be compatible.
Each theme is self-contained in the following files:
Tool | Configuration |
---|---|
tmux | .tmux.conf |
Zsh | .zprezto/modules/prompt/functions/prompt_andrew_setup (at adgoudz/prezto) |
Neovim | .vimrc |
I use a custom 16-color scheme modeled on Base16 and inspired by Solarized and
Tomorrow Night. I've named it "Astra", and while I've adapted it to almost every
tool I use, I need to make time to contribute it back to Base16 tinted-theming.
The scheme is configured in each of the following:
Tool | Configuration |
---|---|
Alacritty | .config/alacritty/alacritty.yml |
Neovim | .vim/colors/base16-astra.vim |
vim-airline | .vim/autoload/airline/themes/base16_astra.vim |
-
asdf-vm/asdf
One version manager to rule them all. -
junegunn/fzf
A fantastic fuzzy-finder for your file system and almost anything else you can think of. -
burntsushi/ripgrep
A regex search tool. Likegrep
,ag
, andpt
, but faster. -
rupa/z
No more creating aliases forcd
-ing into your favorite directories. -
htop-dev/htop
An interactive process viewer. Liketop
, but fancier.