Last active
January 25, 2025 09:33
-
-
Save Raiu/a43bf02e6922045f1bf5f05db960044b to your computer and use it in GitHub Desktop.
Zsh and Vim Environment Setup Script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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