#!/usr/bin/env bash set -e # ==================== # Defaults # ==================== THEME="clean" ADD_NVM=false DRY_RUN=false UPDATE_PLUGINS=false CHECK=false ZSH_DIR="$HOME/.zsh" ZSHRC="$HOME/.zshrc" ZSHRC_LOCAL="$HOME/.zshrc.local" ZSH_CUSTOM="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}" # Detect if we're being piped from curl (no local repo available) PIPED_INSTALL=false if [ -z "$BASH_SOURCE" ] || [ "$BASH_SOURCE" = "bash" ] || [ "$0" = "bash" ]; then PIPED_INSTALL=true fi # If piped, we need to clone the repo first and re-exec from there if $PIPED_INSTALL; then REPO_URL="https://github.com/jjsalinas/dotfiles.git" CLONE_DIR="$HOME/.dotfiles" echo "[INFO] Piped install detected — cloning repo to $CLONE_DIR" if [ -d "$CLONE_DIR" ]; then echo "[INFO] Repo already exists, pulling latest" git -C "$CLONE_DIR" pull --ff-only else git clone "$REPO_URL" "$CLONE_DIR" fi echo "[INFO] Re-executing installer from cloned repo" exec bash "$CLONE_DIR/install.sh" "$@" fi # Running from a local clone — resolve DOTFILES_DIR normally DOTFILES_DIR="$(cd "$(dirname "$0")" && pwd)" # ==================== # Logging # ==================== log_info() { echo "[INFO] $*"; } log_warn() { echo "[WARN] $*"; } log_ok() { echo "[OK] $*"; } log_fail() { echo "[FAIL] $*"; } log_error() { echo "[ERROR] $*" >&2; } run() { if $DRY_RUN; then log_info "[dry-run] $*" else "$@" fi } # ==================== # Help # ==================== print_help() { cat << 'EOF' Dotfiles installer (zsh-focused) Usage: ./install.sh [options] Options: --theme Oh My Zsh theme to use (default: clean) --add-nvm Enable Node Version Manager config --update Update installed plugins (git pull) --check Verify installation without making changes --dry-run Show actions without making changes --help Show this help and exit What this does: • Symlinks zsh configuration from this repo • Installs required zsh plugins • Enables syntax highlighting (green/red commands) • Enables fzf history search • Adds ergonomic keybindings • Works on Ubuntu and Fedora Examples: ./install.sh ./install.sh --theme robbyrussell ./install.sh --add-nvm ./install.sh --theme agnoster --add-nvm --dry-run ./install.sh --update ./install.sh --check One-line install: curl -fsSL https://raw.githubusercontent.com/jjsalinas/dotfiles/main/install.sh | bash curl -fsSL https://raw.githubusercontent.com/jjsalinas/dotfiles/main/install.sh | bash -s -- --theme clean --add-nvm EOF } # ==================== # Argument parsing # ==================== while [[ $# -gt 0 ]]; do case "$1" in --theme) THEME="$2" shift ;; --add-nvm) ADD_NVM=true ;; --update) UPDATE_PLUGINS=true ;; --check) CHECK=true ;; --dry-run) DRY_RUN=true ;; --help) print_help exit 0 ;; *) log_error "Unknown option: $1" exit 1 ;; esac shift done # ==================== # --check mode # ==================== if $CHECK; then log_info "Running installation check..." FAILURES=0 check_ok() { log_ok "$1"; } check_fail() { log_fail "$1"; FAILURES=$((FAILURES + 1)); } # Commands command -v zsh >/dev/null 2>&1 && check_ok "zsh is installed" || check_fail "zsh not found" command -v git >/dev/null 2>&1 && check_ok "git is installed" || check_fail "git not found" command -v fzf >/dev/null 2>&1 && check_ok "fzf is installed" || check_fail "fzf not found" [ -d "$HOME/.oh-my-zsh" ] && check_ok "Oh My Zsh is installed" || check_fail "Oh My Zsh not found at ~/.oh-my-zsh" # Plugins [ -d "$ZSH_CUSTOM/plugins/zsh-syntax-highlighting" ] \ && check_ok "zsh-syntax-highlighting plugin exists" \ || check_fail "zsh-syntax-highlighting plugin missing" [ -d "$ZSH_CUSTOM/plugins/zsh-autosuggestions" ] \ && check_ok "zsh-autosuggestions plugin exists" \ || check_fail "zsh-autosuggestions plugin missing" # Symlinks check_symlink() { local link="$1" target="$2" if [ -L "$link" ] && [ "$(readlink "$link")" = "$target" ]; then check_ok "symlink OK: $link -> $target" elif [ -L "$link" ]; then check_fail "symlink wrong target: $link -> $(readlink "$link") (expected $target)" elif [ -e "$link" ]; then check_fail "exists but not a symlink: $link" else check_fail "symlink missing: $link" fi } check_symlink "$ZSHRC" "$DOTFILES_DIR/zsh/zshrc" check_symlink "$ZSH_DIR/keybindings.zsh" "$DOTFILES_DIR/zsh/keybindings.zsh" check_symlink "$ZSH_DIR/history.zsh" "$DOTFILES_DIR/zsh/history.zsh" check_symlink "$ZSH_DIR/fzf.zsh" "$DOTFILES_DIR/zsh/fzf.zsh" check_symlink "$ZSH_DIR/plugins.zsh" "$DOTFILES_DIR/zsh/plugins.zsh" # .zshrc.local [ -f "$ZSHRC_LOCAL" ] \ && check_ok ".zshrc.local exists" \ || check_fail ".zshrc.local missing (theme not configured)" echo "" if [ "$FAILURES" -eq 0 ]; then log_info "All checks passed." exit 0 else log_error "$FAILURES check(s) failed." exit 1 fi fi # ==================== # Preconditions # ==================== if [ ! -d "$HOME/.oh-my-zsh" ]; then log_error "Oh My Zsh is not installed. Install it first: https://ohmyz.sh" exit 1 fi # ==================== # OS detection # ==================== log_info "Detecting OS" if command -v apt >/dev/null 2>&1; then PKG_INSTALL="sudo apt install -y" elif command -v dnf >/dev/null 2>&1; then PKG_INSTALL="sudo dnf install -y" elif command -v brew >/dev/null 2>&1; then PKG_INSTALL="brew install" else PKG_INSTALL="" fi # ==================== # Dependencies # ==================== log_info "Installing dependencies" if [ -n "$PKG_INSTALL" ]; then command -v git >/dev/null 2>&1 || run $PKG_INSTALL git command -v fzf >/dev/null 2>&1 || run $PKG_INSTALL fzf fi # ==================== # Plugin install/update helper # ==================== install_or_update_plugin() { local name="$1" local url="$2" local dest="$ZSH_CUSTOM/plugins/$name" if [ -d "$dest" ]; then if $UPDATE_PLUGINS; then log_info "Updating $name" run git -C "$dest" pull --ff-only else log_warn "$name already installed (use --update to pull latest)" fi else log_info "Installing $name" run git clone "$url" "$dest" fi } # ==================== # Plugins # ==================== install_or_update_plugin "zsh-syntax-highlighting" \ "https://github.com/zsh-users/zsh-syntax-highlighting" install_or_update_plugin "zsh-autosuggestions" \ "https://github.com/zsh-users/zsh-autosuggestions" # ==================== # Backup existing files # ==================== if [ -f "$ZSHRC" ] && [ ! -L "$ZSHRC" ]; then log_info "Backing up existing .zshrc" run cp "$ZSHRC" "$ZSHRC.backup.$(date +%s)" fi # ==================== # Symlinks # ==================== log_info "Creating zsh config directory" run mkdir -p "$ZSH_DIR" log_info "Linking zsh config files" run ln -sf "$DOTFILES_DIR/zsh/zshrc" "$ZSHRC" run ln -sf "$DOTFILES_DIR/zsh/keybindings.zsh" "$ZSH_DIR/keybindings.zsh" run ln -sf "$DOTFILES_DIR/zsh/history.zsh" "$ZSH_DIR/history.zsh" run ln -sf "$DOTFILES_DIR/zsh/fzf.zsh" "$ZSH_DIR/fzf.zsh" run ln -sf "$DOTFILES_DIR/zsh/plugins.zsh" "$ZSH_DIR/plugins.zsh" if $ADD_NVM; then log_info "Linking NVM config" run ln -sf "$DOTFILES_DIR/zsh/nvm.zsh" "$ZSH_DIR/nvm.zsh" else run rm -f "$ZSH_DIR/nvm.zsh" fi # ==================== # Theme handling # ==================== log_info "Setting theme: $THEME" if ! $DRY_RUN; then # Only write .zshrc.local if theme has changed (or file doesn't exist) current_theme="" if [ -f "$ZSHRC_LOCAL" ]; then current_theme=$(grep -oP '(?<=ZSH_THEME=")[^"]+' "$ZSHRC_LOCAL" 2>/dev/null || true) fi if [ "$current_theme" != "$THEME" ]; then cat << EOF > "$ZSHRC_LOCAL" export ZSH_THEME="$THEME" EOF log_info "Theme set to: $THEME" else log_warn ".zshrc.local already has theme '$THEME', skipping" fi else log_info "[dry-run] would write ~/.zshrc.local with theme $THEME" fi # ==================== # Done # ==================== log_info "Dotfiles installation complete" log_info "Open a new terminal or run: exec zsh" if $DRY_RUN; then log_warn "Dry-run mode enabled — no changes were made" fi