Skip to content

Instantly share code, notes, and snippets.

@Raiu
Last active January 25, 2025 09:33
Show Gist options
  • Save Raiu/a43bf02e6922045f1bf5f05db960044b to your computer and use it in GitHub Desktop.
Save Raiu/a43bf02e6922045f1bf5f05db960044b to your computer and use it in GitHub Desktop.
Zsh and Vim Environment Setup Script
#!/bin/sh
#
# Zsh and Vim Environment Setup Script
# ====================================
#
# This script automates the setup of a remote machine with practical defaults
# for Zsh and Vim while adhering to the XDG Base Directory Specification.
# It focuses on minimal system impact, ensuring configurations are clean,
# isolated, and easy to maintain.
#
# The script is designed to work seamlessly in automated setups, making it
# ideal for provisioning new machines or replicating configurations across environments.
#
# Usage:
# ------
# 1. Download and run the script directly:
# $ sh -c "$(curl -fsSL https://gist.githubusercontent.com/Raiu/a43bf02e6922045f1bf5f05db960044b/raw/setup.zsh)"
# $ sh -c "$(wget -qO- https://gist.githubusercontent.com/Raiu/a43bf02e6922045f1bf5f05db960044b/raw/setup.zsh)"
#
# 2. Run the script with options:
# -q, --quiet : Suppress non-error output for automated setups.
#
# Install in quiet mode for automation:
# $ sh -c "$(curl -fsSL https://gist.githubusercontent.com/Raiu/a43bf02e6922045f1bf5f05db960044b/raw/setup.zsh)" -- -q
# $ sh -c "$(wget -qO- https://gist.githubusercontent.com/Raiu/a43bf02e6922045f1bf5f05db960044b/raw/setup.zsh)" -- -q
#
# Dependencies:
# -------------
# - zsh
# - curl or wget
#
set -e
XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}"
ZDOTDIR="${ZDOTDIR:-$XDG_CONFIG_HOME/zsh}"
ZSH_CACHE_DIR="${XDG_CACHE_HOME}/zsh"
ZSH_DATA_DIR="${XDG_DATA_HOME}/zsh"
ZSH_STATE_DIR="${XDG_STATE_HOME}/zsh"
ANTIDOTE_DIR="${ZDOTDIR}/antidote"
PURE_DIR="${ZDOTDIR}/pure"
VIMDOTDIR="${VIMDOTDIR:-$XDG_CONFIG_HOME/vim}"
VIMDATADIR="${XDG_DATA_HOME}/vim"
VIMRC="$VIMDOTDIR/vimrc"
VIM_PLUG_FILE="${VIMDOTDIR}/autoload/plug.vim"
VIM_PLUG_URL="https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim"
PURE_URL="https://github.com/sindresorhus/pure/archive/refs/heads/main.tar.gz"
ANTIDOTE_URL="https://github.com/mattmc3/antidote/archive/refs/heads/main.tar.gz"
exist() { type "$1" >/dev/null 2>&1; }
log() { [ "$QUIET" -ne 1 ] && echo "$@"; }
error() { echo "Error: $*" >&2; exit 1; }
ZSH_ZSHENV=$(
cat <<'EOF'
process_path() {
reversed_path=$(echo "$1" | tr ':' '\n' | sed '1!G;h;$!d')
unique_path=""
for dir in $(echo "$reversed_path" | tr ':' '\n'); do
case ":$unique_path:" in
*":$dir:"*) ;;
*) unique_path="$dir${unique_path:+:$unique_path}" ;;
esac
done
echo $unique_path
#echo "$unique_path" | tr ':' '\n' | sed '1!G;h;$!d' | tr '\n' ':'
}
bpath="$PATH:${XDG_BIN_HOME:-${HOME}/.local/bin}:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin"
export PATH=$(process_path "$bpath")
# XDG
export XDG_CACHE_HOME="${HOME}/.cache"
export XDG_CONFIG_DIRS="/etc/xdg"
export XDG_CONFIG_HOME="${HOME}/.config"
export XDG_DATA_DIRS="/usr/local/share:/usr/share"
export XDG_DATA_HOME="${HOME}/.local/share"
export XDG_STATE_HOME="${HOME}/.local/state"
# ZSH
export ZDOTDIR="${XDG_CONFIG_HOME}/zsh"
export ZSH_CACHE_DIR="${XDG_CACHE_HOME}/zsh"
export ZSH_CONFIG_DIR="${ZDOTDIR}"
export ZSH_DATA_DIR="${XDG_DATA_HOME}/zsh"
export ZSH_STATE_DIR="${XDG_STATE_HOME}/zsh"
# VIM
export VIMDIR="${VIMDIR:-$XDG_CONFIG_HOME/vim}"
export MYVIMRC="${MYVIMRC:-$VIMDIR/vimrc}"
export VIMINIT="let &runtimepath.=',\$VIMDIR' | source \$MYVIMRC"
# GENERAL
export EDITOR="vim"
EOF
)
ZSH_ZSHRC=$(
cat <<'EOF'
# Helper functions
_exist() { type "$1" >/dev/null 2>&1; }
[[ -f ${ZDOTDIR}/options.zsh ]] && source ${ZDOTDIR}/options.zsh
[[ -f ${ZDOTDIR}/zstyles.zsh ]] && source ${ZDOTDIR}/zstyles.zsh
# Antidote Plugin Manager
source "${ZDOTDIR}/antidote/antidote.zsh"
antidote load
# History
autoload -Uz up-line-or-beginning-search
autoload -Uz down-line-or-beginning-search
zle -N up-line-or-beginning-search
zle -N down-line-or-beginning-search
HISTFILE="${ZSH_STATE_DIR}/history"
HISTSIZE=10000
SAVEHIST=10000
HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND='fg=cyan,bold,underline'
HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_NOT_FOUND='fg=red'
# Pure Prompt
fpath+="${ZDOTDIR}/pure"
autoload -U promptinit; promptinit
prompt pure
# Completion Cache
autoload -Uz compinit && compinit -d "${ZSH_CACHE_DIR}/zcompdump"
if _exist zoxide; then
eval "$(zoxide init zsh)"
fi
# Keymappings
bindkey '\e[3~' delete-char
bindkey '\e[H' beginning-of-line
bindkey '\e[F' end-of-line
bindkey '\e[A' up-line-or-beginning-search
bindkey '\e[B' down-line-or-beginning-search
bindkey '^P' history-beginning-search-backward
bindkey '^N' history-beginning-search-forward
[[ -f "${ZDOTDIR}/functions.zsh" ]] && source "${ZDOTDIR}/functions.zsh"
[[ -f "${ZDOTDIR}/alias.zsh" ]] && source "${ZDOTDIR}/alias.zsh"
EOF
)
ZSH_OPTIONS=$(
cat <<'EOF'
setopt prompt_subst
setopt hist_ignore_all_dups
setopt hist_save_no_dups
setopt share_history
setopt autopushd
setopt pushdignoredups
setopt appendhistory
setopt extended_history
setopt hist_expire_dups_first
setopt auto_cd
EOF
)
ZSH_ZSTYLES=$(
cat <<'EOF'
zstyle ':antidote:bundle' file ${ZDOTDIR:-~}/plugins.list
zstyle ':antidote:static' file ${ZDOTDIR:-~}/plugins.zsh
zstyle ':antidote:bundle' use-friendly-names 'yes'
zstyle ':antidote:plugin:*' defer-options '-p'
EOF
)
ZSH_ALIASES=$(
cat <<'EOF'
alias -g ...='../..'
alias -g ....='../../..'
alias -g .....='../../../..'
alias -g ......='../../../../..'
alias -- -='cd -'
alias 1='cd -1'
alias 2='cd -2'
alias 3='cd -3'
alias 4='cd -4'
alias 5='cd -5'
alias 6='cd -6'
alias 7='cd -7'
alias 8='cd -8'
alias 9='cd -9'
alias md='mkdir -p'
alias rd=rmdir
alias egrep='grep -E --color=auto --exclude-dir={.bzr,CVS,.git,.hg,.svn,.idea,.tox}'
alias fgrep='grep -F --color=auto --exclude-dir={.bzr,CVS,.git,.hg,.svn,.idea,.tox}'
_exist "batcat" && alias bat="batcat"
_exist "fdfind" && alias fd="fdfind"
# use eza
if _exist "eza"; then
alias ls='eza --icons --group-directories-first'
alias lsa='eza -a --icons --group-directories-first'
alias lt='eza -T --group-directories-first --icons --git'
alias lta='eza -Ta --group-directories-first --icons --git'
alias ll='eza -lmh --group-directories-first --color-scale --icons'
alias la='eza -lamhg --group-directories-first --color-scale --icons --git'
alias laa='eza -lamhg@ --group-directories-first --color-scale --icons --git'
alias lx='eza -lbhHigUmuSa@ --group-directories-first --color-scale --icons --git --time-style=long-iso'
else
alias ls='LC_COLLATE=C ls -h --group-directories-first --color=auto'
alias lsa='LC_COLLATE=C ls -Ah --group-directories-first --color=auto'
alias ll='LC_COLLATE=C ls -lh --group-directories-first --color=auto'
alias la='LC_COLLATE=C ls -lAh --group-directories-first --color=auto'
fi
alias zre='source "${ZDOTDIR}/zshrc"'
alias zed='vim "${ZDOTDIR}/zshrc"'
EOF
)
ZSH_FUNCTIONS=$(
cat <<'EOF'
extract() {
local vb=0
while [[ $1 =~ ^- ]]; do
case $1 in
-v|--vb) vb=1 ;;
*) echo "Usage: extract [-v|--vb] <archive> [output_directory]"; return 1 ;;
esac
shift
done
[[ -z $1 || ! -f $1 ]] && { echo "Usage: extract [-v|--vb] <archive> [output_directory]"; return 1; }
local arch="$1" out="${2:-}"
[[ -n $out && ! -d $out ]] && mkdir -p "$out" || { echo "Error: Failed to create directory '$out'."; return 1; }
_run() { _exist "$1" || { echo "Error: $1 is required but not installed."; exit 1; }; "$@"; }
case "$arch" in
*.tar.bz2) _run tar xj${vb:+v}f "$arch" ${out:+-C "$out"} ;;
*.tar.gz|*.tgz) _run tar xz${vb:+v}f "$arch" ${out:+-C "$out"} ;;
*.tar.xz) _run tar xJ${vb:+v}f "$arch" ${out:+-C "$out"} ;;
*.tar) _run tar x${vb:+v}f "$arch" ${out:+-C "$out"} ;;
*.zip) _run unzip ${vb:+-v} "$arch" ${out:+-d "$out"} ;;
*.rar) _run unrar ${vb:+v} x "$arch" ${out:+$out} ;;
*.7z) _run 7z x "$arch" ${out:+-o"$out"} ${vb:+-bb1} ;;
*) echo "Error: Unsupported archive type '$arch'."; return 1 ;;
esac
[[ $vb -eq 1 ]] && echo "Extracted '$arch' to '${out:-$PWD}'."
}
EOF
)
ZSH_PLUGINS=$(
cat <<'EOF'
zsh-users/zsh-completions
zsh-users/zsh-syntax-highlighting
zsh-users/zsh-autosuggestions kind:defer
zsh-users/zsh-history-substring-search kind:defer
EOF
)
VIM_VIMRC=$(
cat <<'EOF'
let $XDG_CONFIG_HOME = exists('$XDG_CONFIG_HOME') ? $XDG_CONFIG_HOME : expand('~/.config')
let $XDG_DATA_HOME = exists('$XDG_DATA_HOME') ? $XDG_DATA_HOME : expand('~/.local/share')
let $VIMDOTDIR = $XDG_CONFIG_HOME . '/vim'
let $VIMDATADIR = $XDG_DATA_HOME . '/vim'
" ====== Plugins ======
call plug#begin()
Plug 'tpope/vim-sensible'
Plug 'sheerun/vim-polyglot'
Plug 'dense-analysis/ale'
Plug 'preservim/nerdtree'
Plug 'itchyny/lightline.vim'
Plug 'joshdick/onedark.vim'
call plug#end()
" ====== Load sensible ======
runtime! plugin/sensible.vim
" ====== Basic Settings ======
syntax on " Enable syntax highlighting
filetype plugin indent on " Enable filetype detection and indentation
set number relativenumber " Show line numbers
set tabstop=4 shiftwidth=4 expandtab " Spaces instead of tabs
set ignorecase smartcase " Case-insensitive search unless uppercase
set hlsearch " Highlight search results
set incsearch " Incremental search
set cursorline " Highlight the current line
set splitbelow splitright " Open splits to the right and below
set mouse=a " Enable mouse support
set clipboard=unnamedplus " Use system clipboard
" ====== Directories and Files ======
set backupdir=$VIMDATADIR/backup//
set undodir=$VIMDATADIR/undo//
set directory=$VIMDATADIR/swap//
set backup
set undofile
set viminfo+=n$VIMDATADIR/viminfo
" ====== NERDTree ======
autocmd VimEnter * if !argc() | NERDTree | endif
autocmd BufWritePost * NERDTreeRefreshRoot
" ====== ALE ======
let g:ale_fix_on_save = 1 " Auto-fix errors on save
let g:ale_lint_on_text_changed = 'never'
let g:ale_lint_on_insert_leave = 1
let g:ale_lint_on_save = 1
let g:ale_completion_enabled = 1 " Enable ALE completion
" Use specific linters
let g:ale_linters = {
\ 'python': ['flake8', 'pylint'],
\ 'javascript': ['eslint'],
\ 'sh': ['shellcheck']
\ }
" Use specific fixers
let g:ale_fixers = {
\ '*': ['remove_trailing_lines', 'trim_whitespace'],
\ 'javascript': ['prettier'],
\ 'python': ['black', 'isort']
\ }
" ====== Colorscheme ======
if !empty(globpath(&rtp, "colors/onedark.vim"))
colorscheme onedark
endif
" ====== Lightline Configuration ======
let g:lightline = {
\ 'colorscheme': 'onedark',
\ 'active': {
\ 'left': [ ['mode', 'paste'], ['readonly', 'filename', 'modified'] ],
\ 'right': [ ['lineinfo'], ['percent'], ['filetype'] ]
\ },
\ 'inactive': {
\ 'left': [ ['filename'] ],
\ 'right': [ ['lineinfo'], ['percent'] ]
\ }
\ }
" Enable Powerline-like separators
let g:lightline.separator = { 'left': "\ue0b0", 'right': "\ue0b2" }
let g:lightline.subseparator = { 'left': "\ue0b1", 'right': "\ue0b3" }
" ALE Statusline integration
let g:lightline.component_expand = {
\ 'linter_checking': 'ale#statusline#Count'
\ }
let g:lightline.component_type = {
\ 'linter_checking': 'error'
\ }
" Enable bracketed paste mode (Preferred)
if &term =~ 'xterm' || &term =~ 'screen' || &term =~ 'tmux'
set ttymouse=sgr
set pastetoggle=<F2> " Manual toggle for emergencies
set noautoindent " Disable auto-indent during paste
set smartindent " Restore smart indent after paste
" Bracketed Paste
let &t_BE = "\e[?2004h" " Enable bracketed paste
let &t_BD = "\e[?2004l" " Disable bracketed paste
let &t_PS = "\e[200~" " Paste start
let &t_PE = "\e[201~" " Paste end
endif
" Highlight status when paste mode is active
set ttimeoutlen=10
set showmode
autocmd OptionSet paste if &paste | set statusline=[PASTE] | else | set statusline= | endif
" ====== Key Mappings ======
let mapleader = ","
" Switch between Normal and Insert mode with Ctrl-Space
nmap <C-@> a
imap <C-@> <Esc>
" Improved Search and Replace
nnoremap <leader>r :%s//g<Left><Left>
vnoremap <leader>r :s//g<Left><Left>
" Easy file switching
nnoremap <leader>n :bnext<CR>
nnoremap <leader>p :bprev<CR>
nnoremap <leader>d :bdelete<CR>
nnoremap <leader>f :Files<CR>
" Save and Quit shortcuts
nnoremap <leader>w :w<CR>
nnoremap <leader>q :q<CR>
" Reload Vim configuration
nnoremap <leader>sv :source $MYVIMRC<CR>
" NERDTree
nnoremap <leader>e :NERDTreeToggle<CR>
nnoremap <leader>ef :NERDTreeFind<CR>
" ALE
nnoremap <leader>an :ALENext<CR>
nnoremap <leader>ap :ALEPrevious<CR>
" Yank selected text into register (z)
vnoremap <leader>y "zy
" Search-replace using text from register (z)
nnoremap <leader>c :<C-u>let @/ = escape(@z, '/\\')<CR>:%s/<C-r>=@z<CR>/<C-r>=@z<CR>/g<Left><Left>
vnoremap <leader>c :<C-u>let @/ = escape(@z, '/\\')<CR>:'<,'>s/<C-r>=@z<CR>/<C-r>=@z<CR>/g<Left><Left>
" Search-replace using selection as search pattern
vnoremap <leader>r "hy:let @/ = escape(@h, '/\\.*^$[]~')<CR>:%s/<C-r>=@/<CR>/<C-r>=@/<CR>/g<Left><Left>
" ====== Plugin Specific Settings ======
EOF
)
symlink() {
[ ! -d "$(dirname "$2")" ] && mkdir -p "$(dirname "$2")"
[ -L "$2" ] && [ "$(readlink -f "$2")" = "$(readlink -f "$1")" ] && return 0
[ -e "$2" ] && [ ! -L "$2" ] && mv "$2" "$2.bak"
ln -sf "$1" "$2"
}
download_pkg() {
[ -z "$2" ] || [ "$2" = "/" ] && error "'$2' is not a valid path."
[ -d "$2" ] && rm -rf "$2"
mkdir -p "$2"
TMP="$(mktemp -d)" || error "Failed to create temp directory."
if exist curl; then
curl -sSLo "$TMP/pkg.tar.gz" "$1" || error "Failed to download $1 with curl."
elif exist wget; then
wget -qO "$TMP/pkg.tar.gz" "$1" || error "Failed to download $1 with wget."
else
error "curl or wget is required."
fi
tar -xzf "$TMP/pkg.tar.gz" -C "$2" --strip-components=1 || error "Failed to extract $1 to $2."
rm -rf "$TMP"
}
download_file() {
if exist curl; then
curl -sS -fLo "$2" "$1" || error "Failed to download $1 with curl."
elif exist wget; then
wget -qO "$2" "$1" || error "Failed to download $1 with wget."
else
error "curl or wget is required."
fi
}
check_dependencies() {
exist zsh || error "zsh is required but not installed."
exist curl || exist wget || error "Either curl or wget is required but neither is installed."
}
create_xdg_dirs() {
mkdir -p "${XDG_CACHE_HOME}" "${XDG_CONFIG_HOME}" "${XDG_DATA_HOME}" "${XDG_STATE_HOME}"
mkdir -p "${ZDOTDIR}" "${ZSH_CACHE_DIR}" "${ZSH_DATA_DIR}" "${ZSH_STATE_DIR}"
}
install_antidote() { download_pkg "$ANTIDOTE_URL" "$ANTIDOTE_DIR"; echo "$ZSH_PLUGINS" >"${ZDOTDIR}/plugins.list"; }
install_pure_prompt() { download_pkg "$PURE_URL" "$PURE_DIR"; }
install_vim() {
mkdir -p "${VIMDOTDIR}/after" "${VIMDOTDIR}/autoload" "${VIMDOTDIR}/colors" "${VIMDOTDIR}/plugin"
mkdir -p "${VIMDATADIR}/backup" "${VIMDATADIR}/swap" "${VIMDATADIR}/undo"
download_file "$VIM_PLUG_URL" "$VIM_PLUG_FILE"
echo "${VIM_VIMRC}" >"${VIMRC}"
vim -es -u "$VIMRC" -i NONE -c "PlugInstall --sync" -c "qa" >/dev/null 2>&1 || true
}
deploy_zsh_files() {
echo "${ZSH_ZSHENV}" >"${ZDOTDIR}/zshenv"
echo "${ZSH_ZSHRC}" >"${ZDOTDIR}/zshrc"
echo "${ZSH_OPTIONS}" >"${ZDOTDIR}/options.zsh"
echo "${ZSH_ZSTYLES}" >"${ZDOTDIR}/zstyles.zsh"
echo "${ZSH_ALIASES}" >"${ZDOTDIR}/alias.zsh"
echo "${ZSH_FUNCTIONS}" >"${ZDOTDIR}/functions.zsh"
symlink "${ZDOTDIR}/zshenv" "${ZDOTDIR}/.zshenv"
symlink "${ZDOTDIR}/zshrc" "${ZDOTDIR}/.zshrc"
}
zshenv_symlink() { grep -q 'XDG_CONFIG_HOME' /etc/zsh/zshenv 2>/dev/null || symlink "${ZDOTDIR}/zshenv" "${HOME}/.zshenv" ;}
main() {
QUIET=0
case "$1" in
-q | --quiet) QUIET=1 ;;
"") ;;
*) error "Invalid argument: $1" ;;
esac
log "Setting up..."
log "* Verifying dependencies..."
check_dependencies
log "* Creating XDG directories..."
create_xdg_dirs
log "* Deploying Zsh configuration..."
deploy_zsh_files
log "* Installing Antidote..."
install_antidote
log "* Installing Pure Prompt..."
install_pure_prompt
log "* Installing Vim..."
install_vim
log "* Verifying zshenv symlink..."
zshenv_symlink
log "Setup complete! Restart your shell or run: exec zsh"
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment