My Terminal Setup: Dotfiles That Actually Work Across Machines
Photo by Alan W / Unsplash
I got tired of manually setting up my shell environment every time I touched a new machine. So I built a dotfiles repo that handles everything with a single curl command.
https://github.com/spaceCabbage/dotfiles
Most of the tools i install are referenced in my Awesome-CLI article.
The One-Liner Installation
curl -fsSL https://raw.githubusercontent.com/spaceCabbage/dotfiles/arch/install.sh | bash
That's it. Fresh install to working dev environment in one command. It also works on Debian servers with some automatic package mapping.
What install.sh Actually Does
The script runs through a pretty straightforward sequence:
- Detects whether you're on Arch or Debian
- Installs git and yay (on Arch) if missing
- Clones the repo to ~/dotfiles
- Reads packages.txt and installs everything via yay or apt
- Installs starship, rustup, bun, and uv via their official curl installers
- Creates symlinks for all the config files
- Backs up any existing configs to ~/.dotfiles-backup/ with timestamps
- Creates ~/.bashrc_local for machine-specific stuff that shouldn't be in git
The Debian handling is worth mentioning. Some packages have different names (python vs python3, go vs golang), and some things like eza and zoxide aren't in apt at all. The script maps package names where needed and falls back to cargo for the rust-based tools on Debian.
The Bash Setup
.bashrc
The main bashrc is minimal. It sources system defaults, loads aliases, sets up environment variables, and initializes the tools that need shell integration. Key bits:
export EDITOR='nvim'
export VISUAL='nvim'
# History config
PROMPT_COMMAND='history -a'
export HISTCONTROL=ignoredups
export HISTFILESIZE=50000
export HISTSIZE=${HISTFILESIZE}
# The tools that make the shell usable
eval "$(starship init bash)"
eval "$(direnv hook bash)"
eval "$(zoxide init bash --cmd cd)"
# Machine-specific config loads last
if [ -f ~/.bashrc_local ]; then
. ~/.bashrc_local
fi
The ~/.bashrc_local pattern is the key to keeping one dotfiles repo across multiple machines. SSH aliases for work servers, API keys, whatever. It gets sourced but never committed.
.bash_aliases
This is where most of the actual config lives. Some highlights:
Quick edits
alias ebrc='nvim ~/.bashrc'
alias eba='nvim ~/.bash_aliases'
alias sbrc='source ~/.bashrc'
Modern replacements for old commands:
alias ll="eza -lhaF --icons --git --group-directories-first --git-repos --git"
alias ls="eza --icons --no-permissions --no-user --color=always"
alias cat="bat -P"
Navigation:
alias cdr='cd "$(git rev-parse --show-toplevel)"' # Jump to repo root
alias bd='cd "$OLDPWD"' # Back to previous dir
alias yy="yazi" # File manager
The sync function
This is the one I actually use constantly. It handles the annoying git dance of stash-pull-rebase-pop-commit-push:
sync_repo() {
local repo_dir="${1:-.}"
local commit_msg="${2:-Update $(basename "$repo_dir") - $(date +%Y-%m-%d\ %H:%M:%S)}"
cd "$repo_dir" || return 1
git fetch origin 2>/dev/null
# Check for remote and local changes
local remote_changes=$(git rev-list HEAD..@{u} --count 2>/dev/null || echo "0")
local has_local_changes="false"
[[ -n $(git status -s) ]] && has_local_changes="true"
# Stash if needed, pull, pop stash, commit, push
# ... handles all the edge cases
}
syncdot() {
sync_repo "$HOME/dotfiles" "Update dotfiles"
}
alias sd="syncdot"
Type sd in any terminal and my dotfiles sync. Same pattern for notes with sn.
Notes System:
I keep markdown notes in ~/Documents/notes and sync them with git. The notes function creates new notes with a template, and fn uses fzf with glow preview to browse them
fn() {
local selected
selected=$(find "$NOTES_DIR" -type f \( -name "*.md" -o -name "*.txt" \) |
sed "s|$NOTES_DIR/||" |
fzf --preview "glow $NOTES_DIR/{}" \
--preview-window="right:70%:wrap" \
--header 'alt-p: toggle preview | alt-j/k: scroll | enter: open')
[[ -n "$selected" ]] && glow -p "$NOTES_DIR/$selected"
}
Package management with fzf:
yayi() {
pkg_names=$(pacman -Slq | fzf --multi \
--preview 'pacman -Sii {1}' \
--preview-window 'down:45%:wrap')
if [[ -n "$pkg_names" ]]; then
echo "$pkg_names" | tr '\n' ' ' | xargs sudo pacman -S --noconfirm
fi
}
Package management with fzf
Browse all available packages with fuzzy search and previews. Multi-select with tab.
yayi() {
pkg_names=$(pacman -Slq | fzf --multi \
--preview 'pacman -Sii {1}' \
--preview-window 'down:45%:wrap')
if [[ -n "$pkg_names" ]]; then
echo "$pkg_names" | tr '\n' ' ' | xargs sudo pacman -S --noconfirm
fi
}
Starship Prompt
The prompt shows username, hostname (useful when SSH'd somewhere), git branch/status, language versions, and command duration. Gruvbox colors because I'm not a monster.
[username]
show_always = true
format = "[${user}](fg:#83a598)"
[hostname]
ssh_only = false
format = "[@](fg:#688080)[${hostname}](fg:#688080) "
[directory]
style = "fg:#689d6a"
truncation_length = 4
[git_branch]
symbol = " "
style = "fg:#79740e"
[character]
success_symbol = "[❯](fg:#83a598)"
error_symbol = "[❯](fg:#fb4934)"
The SSH indicator is nice. When you're on a remote machine, you see SSH in yellow before everything else.
Tmux Config
Prefix is C-a instead of C-b because reaching for b is annoying.
unbind C-b
set -g prefix C-a
bind C-a send-prefix
Window navigation with Alt-j and Alt-k:
bind -n M-j next-window
bind -n M-k previous-window
Splits that open in the current directory (the default behavior of opening in ~ is useless):
bind ) split-window -c "#{pane_current_path}"
bind ( split-window -h -c "#{pane_current_path}"
bind c new-window -c "#{pane_current_path}"
Vi mode for copy:
set-window-option -g mode-keys vi
bind-key -T copy-mode-vi v send-keys -X begin-selection
bind-key -T copy-mode-vi y send-keys -X copy-pipe-and-cancel
Plugins via TPM: sensible defaults, vim-tmux-navigator for seamless pane/split navigation, tmux-yank for clipboard, and gruvbox theme.
Package List
The packages.txt file is intentionally short. Just the stuff I actually use:
- git
- curl
- wget
- tmux
- zoxide
- fzf
- neovim
- eza
- ripgrep
- bat
- yazi
- jq
- btop
- duf
- tldr
- docker/docker-compose
- python
- golang
- glow
Everything else (starship, rustup, bun, uv) gets installed via curl because the official installers handle updates better than distro packages.
Running It Again
The script is idempotent. Run it as many times as you want.
Existing correct symlinks get skipped. Wrong symlinks or regular files get backed up before replacing, packages use --needed so already-installed ones are skippedm, curl-installed tools check if the binary exists first
New machine? curl | bash. Updated the repo? sd to sync, then run ./install.sh again.
Done.
Full setup lives at ithub.com/spaceCabbage/dotfiles on the arch branch.
Your Incognito Mode Is Lying to You
Newer →16 CLI Tools That Replaced My GUI Habits