Development

Dotfiles Refinement: Cleaner, Smarter, Faster

Transform your shell into a pluggable architecture—introducing loader patterns, focused functions, and a minimal prompt that gets out of your way while boosting productivity

A Year Later, A Simpler Setup

Since my last update in 2015, I’ve had time to live with the modular dotfiles system. It worked. Onboarding was faster, the install scripts were smarter, and I had shell support for both Bash and Zsh.

But even well-organized setups can start to rot. Too many aliases, too many .zshrc conditionals, and a few copy-pasted hacks started to creep in. By late 2016, I felt the need to simplify.

So this wasn’t a rebuild. It was a focused refinement. Fewer features, clearer structure, and a setup I could actually understand six months later.

This post documents how I tightened the screws.

From Organized to Loadable

My 2015 setup was topic-based, with folders like bash/, zsh/, git/, and an install script to symlink everything. But each shell file still sourced other files explicitly:

source "$DOTFILES/zsh/aliases.zsh"
source "$DOTFILES/zsh/functions.zsh"

Now, I’ve introduced a loader pattern. In zsh/load.zsh:

for config_file ($ZSH/*.zsh); do
  source $config_file
done

This small shift makes everything cleaner. Adding new functionality is just dropping a .zsh file in the folder. No need to edit multiple files to register it.

BeforeAfter
Manual source entriesAutomatic loader via loop
Hardcoded dependenciesDrop-in config structure
More friction to extendEasily pluggable architecture

It feels like dependency injection, but for your shell.

Smaller, More Focused Functions

The biggest win from this cycle was cutting. I rewrote most of my shell functions:

  • extract for decompressing archives
  • take for creating and entering directories
  • path-append for safely editing $PATH

Each function lives in its own file under zsh/functions/. This means I can test, replace, or remove one without affecting the rest.

# extract.zsh
extract() {
  case $1 in
    *.tar.bz2)   tar xjf $1     ;;
    *.tar.gz)    tar xzf $1     ;;
    *.bz2)       bunzip2 $1     ;;
    *.rar)       unrar x $1     ;;
    *.gz)        gunzip $1      ;;
    *.tar)       tar xf $1      ;;
    *.tbz2)      tar xjf $1     ;;
    *.tgz)       tar xzf $1     ;;
    *.zip)       unzip $1       ;;
    *.Z)         uncompress $1  ;;
    *)           echo "don't know how to extract '$1'..." ;;
  esac
}
FunctionPurpose
extractDecompress any common archive type
takemkdir && cd in one
path-appendSafely extend PATH

Writing functions as standalone files was the change I didn’t know I needed.

A Prompt That Gets Out of the Way

I used to tinker with the prompt a lot. In 2015, I had a verbose, colored PS1 setup showing time, path, Git branch, and exit code. It was useful — but noisy.

Now I use a much simpler prompt. Just the essentials:

autoload -Uz vcs_info
precmd() { vcs_info }
PROMPT='%n@%m %1~ ${vcs_info_msg_0_}%# '

This prompt:

  • Uses vcs_info to show Git state when needed
  • Shows user, host, and current directory
  • Keeps the screen focused on the command, not decorations
Prompt ElementDescription
%nUsername
%mHostname
%1~Truncated path (last dir only)
vcs_infoGit branch/status

More signal, less noise.

Install Script Now Even Dumber (in a Good Way)

The original install.sh grew too smart. It had conditionals, dependency checks, platform switches. Now it’s dumber — but better.

find * -name "*.symlink" | while read file; do
  ln -sf "$file" "$HOME/.$(basename "$file" ".symlink")"
done

I moved platform-specific setups elsewhere. This lets install.sh do just one job: create symlinks. It’s composable now.

Old install.shNew install.sh
Multi-purpose scriptJust symlinks
Conditional logicClean loop
OS-dependentPortable + minimal

This script now runs the same on every machine. Predictability > cleverness.

What I’ve Learned

This update wasn’t about adding — it was about subtracting. Every improvement came from asking: what can I remove?

Instead of building a framework, I’ve built a system that:

  • Has no magic
  • Is easy to onboard into
  • Can grow without friction
  • Looks boring (and that’s a feature)

I’m not chasing perfection. Just stability and sanity.

Check out the diff on GitHub