From 40d908a7d12356c1b06ad3f1feec737fc04cd294 Mon Sep 17 00:00:00 2001 From: "Aaron D. Lee" Date: Sun, 14 Dec 2025 21:19:00 -0500 Subject: [PATCH] Additional quality of life and deployment improvments. --- .gitconfig.template | 45 ++++ README.md | 36 ++- bin/dotfiles-doctor.sh | 479 ++++++++++++++++++++++++++++++++++++++++ bin/dotfiles-version.sh | 227 +++++++++++++++++++ docs/SETUP_GUIDE.md | 135 ++++++++--- dotfiles.conf | 16 +- install.sh | 219 +++++++++++++++++- 7 files changed, 1120 insertions(+), 37 deletions(-) create mode 100644 .gitconfig.template create mode 100644 bin/dotfiles-doctor.sh create mode 100644 bin/dotfiles-version.sh diff --git a/.gitconfig.template b/.gitconfig.template new file mode 100644 index 0000000..104cc74 --- /dev/null +++ b/.gitconfig.template @@ -0,0 +1,45 @@ +# ============================================================================ +# Git Configuration Template +# ============================================================================ +# This file is a TEMPLATE. The actual .gitconfig is generated by install.sh +# based on settings in dotfiles.conf. +# +# To customize, edit dotfiles.conf: +# GIT_USER_NAME="Your Name" +# GIT_USER_EMAIL="you@example.com" +# GIT_DEFAULT_BRANCH="main" +# GIT_CREDENTIAL_HELPER="store" +# +# Then re-run: ./install.sh +# ============================================================================ + +[init] + defaultBranch = master + +[user] + # Generated from dotfiles.conf or prompted during install + email = + name = + +[credential] + helper = store + +[core] + editor = vim + autocrlf = input + +[pull] + rebase = false + +[push] + default = current + +[alias] + st = status + co = checkout + br = branch + ci = commit + lg = log --oneline --graph --decorate --all + unstage = reset HEAD -- + last = log -1 HEAD + visual = !gitk diff --git a/README.md b/README.md index 66c0160..65912ec 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,22 @@ git clone https://github.com/adlee-was-taken/dotfiles.git ~/.dotfiles cd ~/.dotfiles && ./install.sh ``` -The installer backs up existing configs, installs oh-my-zsh, and creates symlinks. +The installer backs up existing configs, installs oh-my-zsh + plugins, configures git, and creates symlinks. + +### Install Options + +```bash +./install.sh --skip-deps # Skip dependency check (re-runs) +./install.sh --uninstall # Remove symlinks +./install.sh --uninstall --purge # Remove everything +``` ## Repository Layout ``` dotfiles/ ├── install.sh # Main installer +├── dotfiles.conf # Configuration (fork-friendly) ├── zsh/ │ ├── .zshrc # Shell config │ ├── themes/adlee.zsh-theme @@ -44,13 +53,26 @@ dotfiles/ ├── espanso/ │ ├── config/default.yml │ └── match/base.yml # 100+ snippets -├── git/.gitconfig +├── git/.gitconfig.template # Git config template ├── vim/.vimrc ├── tmux/.tmux.conf ├── bin/ # Helper scripts +│ ├── dotfiles-doctor.sh # Health checker +│ ├── dotfiles-version.sh # Version checker +│ └── update-dotfiles.sh └── docs/ # Extended docs ``` +## Health Check + +Verify your installation: + +```bash +dotfiles-doctor.sh # Run diagnostics +dotfiles-doctor.sh --fix # Auto-fix issues +dotfiles-version.sh # Check for updates +``` + ## Espanso Snippets All triggers use `..` prefix to avoid accidents. @@ -115,11 +137,17 @@ snap-check-limine # Verify boot menu sync ## Troubleshooting +```bash +dotfiles-doctor.sh --fix # Auto-diagnose and fix +``` + | Issue | Fix | |-------|-----| -| Theme not loading | Check `ZSH_THEME="adlee"` in ~/.zshrc, then `source ~/.zshrc` | +| Theme not loading | `dotfiles-doctor.sh --fix` | +| Zsh plugins missing | `./install.sh` (auto-installs now) | | Espanso not working | `espanso status`, then `espanso restart` | -| Broken symlinks | `cd ~/.dotfiles && ./install.sh` | +| Broken symlinks | `./install.sh` | +| Want to uninstall | `./install.sh --uninstall` | ## License diff --git a/bin/dotfiles-doctor.sh b/bin/dotfiles-doctor.sh new file mode 100644 index 0000000..1023752 --- /dev/null +++ b/bin/dotfiles-doctor.sh @@ -0,0 +1,479 @@ +#!/usr/bin/env bash +# ============================================================================ +# Dotfiles Doctor - Diagnostic Tool +# ============================================================================ +# Checks the health of your dotfiles installation +# +# Usage: +# dotfiles-doctor.sh # Run all checks +# dotfiles-doctor.sh --fix # Attempt to fix issues +# dotfiles-doctor.sh --quiet # Only show errors +# ============================================================================ + +set -e + +# ============================================================================ +# Options +# ============================================================================ + +FIX_MODE=false +QUIET_MODE=false + +for arg in "$@"; do + case "$arg" in + --fix) + FIX_MODE=true + ;; + --quiet|-q) + QUIET_MODE=true + ;; + --help|-h) + echo "Usage: $0 [OPTIONS]" + echo + echo "Options:" + echo " --fix Attempt to automatically fix issues" + echo " --quiet Only show errors and warnings" + echo " --help Show this help message" + echo + exit 0 + ;; + esac +done + +# ============================================================================ +# Load Configuration +# ============================================================================ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DOTFILES_CONF="${SCRIPT_DIR}/../dotfiles.conf" +[[ -f "$DOTFILES_CONF" ]] || DOTFILES_CONF="${SCRIPT_DIR}/dotfiles.conf" +[[ -f "$DOTFILES_CONF" ]] || DOTFILES_CONF="$HOME/.dotfiles/dotfiles.conf" + +if [[ -f "$DOTFILES_CONF" ]]; then + source "$DOTFILES_CONF" +else + DOTFILES_DIR="$HOME/.dotfiles" + DOTFILES_VERSION="unknown" + ZSH_THEME_NAME="adlee" +fi + +# ============================================================================ +# Colors +# ============================================================================ + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' + +# ============================================================================ +# Counters +# ============================================================================ + +PASS_COUNT=0 +WARN_COUNT=0 +FAIL_COUNT=0 + +# ============================================================================ +# Helper Functions +# ============================================================================ + +print_header() { + if [[ "$QUIET_MODE" != true ]]; then + echo -e "\n${BLUE}╔════════════════════════════════════════════════════════════╗${NC}" + echo -e "${BLUE}║${NC} Dotfiles Doctor ${CYAN}v${DOTFILES_VERSION}${NC} ${BLUE}║${NC}" + echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}\n" + fi +} + +print_section() { + if [[ "$QUIET_MODE" != true ]]; then + echo -e "\n${BLUE}━━━ $1 ━━━${NC}" + fi +} + +pass() { + ((PASS_COUNT++)) + if [[ "$QUIET_MODE" != true ]]; then + echo -e "${GREEN}✓${NC} $1" + fi +} + +warn() { + ((WARN_COUNT++)) + echo -e "${YELLOW}⚠${NC} $1" +} + +fail() { + ((FAIL_COUNT++)) + echo -e "${RED}✗${NC} $1" +} + +info() { + if [[ "$QUIET_MODE" != true ]]; then + echo -e "${CYAN}ℹ${NC} $1" + fi +} + +# ============================================================================ +# Check Functions +# ============================================================================ + +check_dotfiles_dir() { + print_section "Dotfiles Directory" + + if [[ -d "$DOTFILES_DIR" ]]; then + pass "Dotfiles directory exists: $DOTFILES_DIR" + + # Check if it's a git repo + if [[ -d "$DOTFILES_DIR/.git" ]]; then + pass "Is a git repository" + + # Check for uncommitted changes + cd "$DOTFILES_DIR" + if git diff --quiet 2>/dev/null; then + pass "No uncommitted changes" + else + warn "Uncommitted changes in dotfiles" + fi + + # Check if up to date with remote + git fetch origin --quiet 2>/dev/null || true + local local_hash=$(git rev-parse HEAD 2>/dev/null) + local remote_hash=$(git rev-parse origin/${DOTFILES_BRANCH:-main} 2>/dev/null || echo "") + + if [[ -n "$remote_hash" && "$local_hash" == "$remote_hash" ]]; then + pass "Up to date with remote" + elif [[ -n "$remote_hash" ]]; then + warn "Behind remote (run: cd ~/.dotfiles && git pull)" + fi + cd - > /dev/null + else + warn "Not a git repository" + fi + + # Check config file + if [[ -f "$DOTFILES_DIR/dotfiles.conf" ]]; then + pass "Config file exists: dotfiles.conf" + else + fail "Config file missing: dotfiles.conf" + fi + else + fail "Dotfiles directory not found: $DOTFILES_DIR" + fi +} + +check_symlinks() { + print_section "Symlinks" + + local symlinks=( + "$HOME/.zshrc:$DOTFILES_DIR/zsh/.zshrc" + "$HOME/.gitconfig:$DOTFILES_DIR/git/.gitconfig" + "$HOME/.vimrc:$DOTFILES_DIR/vim/.vimrc" + "$HOME/.tmux.conf:$DOTFILES_DIR/tmux/.tmux.conf" + "$HOME/.oh-my-zsh/themes/${ZSH_THEME_NAME}.zsh-theme:$DOTFILES_DIR/zsh/themes/${ZSH_THEME_NAME}.zsh-theme" + ) + + local valid_count=0 + local total_count=0 + + for entry in "${symlinks[@]}"; do + local link="${entry%%:*}" + local target="${entry##*:}" + local name=$(basename "$link") + ((total_count++)) + + if [[ -L "$link" ]]; then + local actual_target=$(readlink -f "$link" 2>/dev/null) + local expected_target=$(readlink -f "$target" 2>/dev/null) + + if [[ "$actual_target" == "$expected_target" ]]; then + pass "Symlink valid: $name" + ((valid_count++)) + else + warn "Symlink points elsewhere: $name" + info " Expected: $target" + info " Actual: $actual_target" + fi + elif [[ -f "$link" ]]; then + warn "Regular file (not symlink): $name" + if [[ "$FIX_MODE" == true ]]; then + if [[ -f "$target" ]]; then + mv "$link" "$link.backup" + ln -sf "$target" "$link" + pass "Fixed: $name (backup saved)" + ((valid_count++)) + fi + fi + elif [[ -f "$target" ]]; then + fail "Symlink missing: $name" + if [[ "$FIX_MODE" == true ]]; then + ln -sf "$target" "$link" + pass "Fixed: Created symlink for $name" + ((valid_count++)) + fi + else + info "Source not present: $name (optional)" + fi + done + + # Check espanso symlink + if [[ -L "$HOME/.config/espanso" ]]; then + pass "Symlink valid: espanso config" + elif [[ -d "$HOME/.config/espanso" ]]; then + warn "Espanso config is directory (not symlink)" + elif [[ -d "$DOTFILES_DIR/espanso" ]]; then + fail "Espanso symlink missing" + fi + + info "Symlinks: $valid_count/$total_count valid" +} + +check_shell() { + print_section "Shell" + + # Check current shell + if [[ "$SHELL" == *"zsh"* ]]; then + pass "Default shell is zsh" + else + warn "Default shell is not zsh: $SHELL" + info " Change with: chsh -s \$(which zsh)" + fi + + # Check oh-my-zsh + if [[ -d "$HOME/.oh-my-zsh" ]]; then + pass "oh-my-zsh installed" + else + fail "oh-my-zsh not installed" + fi + + # Check theme + if [[ -f "$HOME/.oh-my-zsh/themes/${ZSH_THEME_NAME}.zsh-theme" ]]; then + pass "Theme installed: ${ZSH_THEME_NAME}" + else + fail "Theme missing: ${ZSH_THEME_NAME}" + fi + + # Check ZSH_THEME in .zshrc + if grep -q "ZSH_THEME=\"${ZSH_THEME_NAME}\"" "$HOME/.zshrc" 2>/dev/null; then + pass "Theme configured in .zshrc" + else + warn "Theme may not be configured in .zshrc" + fi +} + +check_zsh_plugins() { + print_section "Zsh Plugins" + + local custom_dir="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins" + + # zsh-autosuggestions + if [[ -d "$custom_dir/zsh-autosuggestions" ]]; then + pass "Plugin installed: zsh-autosuggestions" + else + fail "Plugin missing: zsh-autosuggestions" + if [[ "$FIX_MODE" == true ]]; then + git clone --depth 1 https://github.com/zsh-users/zsh-autosuggestions "$custom_dir/zsh-autosuggestions" + pass "Fixed: Installed zsh-autosuggestions" + else + info " Install: git clone https://github.com/zsh-users/zsh-autosuggestions $custom_dir/zsh-autosuggestions" + fi + fi + + # zsh-syntax-highlighting + if [[ -d "$custom_dir/zsh-syntax-highlighting" ]]; then + pass "Plugin installed: zsh-syntax-highlighting" + else + fail "Plugin missing: zsh-syntax-highlighting" + if [[ "$FIX_MODE" == true ]]; then + git clone --depth 1 https://github.com/zsh-users/zsh-syntax-highlighting "$custom_dir/zsh-syntax-highlighting" + pass "Fixed: Installed zsh-syntax-highlighting" + else + info " Install: git clone https://github.com/zsh-users/zsh-syntax-highlighting $custom_dir/zsh-syntax-highlighting" + fi + fi +} + +check_git() { + print_section "Git Configuration" + + # Check git installed + if command -v git &>/dev/null; then + pass "git installed: $(git --version | cut -d' ' -f3)" + else + fail "git not installed" + return + fi + + # Check user.name + local git_name=$(git config --global user.name 2>/dev/null) + if [[ -n "$git_name" ]]; then + pass "Git user.name: $git_name" + else + fail "Git user.name not configured" + info " Set with: git config --global user.name \"Your Name\"" + fi + + # Check user.email + local git_email=$(git config --global user.email 2>/dev/null) + if [[ -n "$git_email" ]]; then + pass "Git user.email: $git_email" + else + fail "Git user.email not configured" + info " Set with: git config --global user.email \"you@example.com\"" + fi + + # Check credential helper + local cred_helper=$(git config --global credential.helper 2>/dev/null) + if [[ -n "$cred_helper" ]]; then + pass "Git credential helper: $cred_helper" + else + warn "Git credential helper not configured" + fi +} + +check_espanso() { + print_section "Espanso" + + if command -v espanso &>/dev/null; then + pass "espanso installed: $(espanso --version 2>/dev/null | head -1)" + + # Check if running + if espanso status 2>/dev/null | grep -q "running"; then + pass "espanso service running" + else + warn "espanso service not running" + info " Start with: espanso service start" + fi + + # Check config + if [[ -f "$HOME/.config/espanso/match/base.yml" ]]; then + pass "espanso config present" + else + warn "espanso base.yml not found" + fi + else + info "espanso not installed (optional)" + fi +} + +check_optional_tools() { + print_section "Optional Tools" + + # fzf + if command -v fzf &>/dev/null; then + pass "fzf installed" + else + info "fzf not installed (optional)" + fi + + # bat/batcat + if command -v bat &>/dev/null || command -v batcat &>/dev/null; then + pass "bat installed" + else + info "bat not installed (optional)" + fi + + # eza + if command -v eza &>/dev/null; then + pass "eza installed" + else + info "eza not installed (optional)" + fi + + # fd + if command -v fd &>/dev/null; then + pass "fd installed" + else + info "fd not installed (optional)" + fi +} + +check_bin_scripts() { + print_section "Bin Scripts" + + local bin_dir="$HOME/.local/bin" + + if [[ -d "$bin_dir" ]]; then + local script_count=0 + local valid_count=0 + + for script in "$DOTFILES_DIR/bin"/*; do + if [[ -f "$script" ]]; then + ((script_count++)) + local name=$(basename "$script") + local link="$bin_dir/$name" + + if [[ -L "$link" ]]; then + ((valid_count++)) + elif [[ -f "$link" ]]; then + warn "Script is regular file: $name" + else + fail "Script not linked: $name" + fi + fi + done + + if [[ $script_count -gt 0 ]]; then + pass "Bin scripts: $valid_count/$script_count linked" + fi + + # Check PATH + if [[ ":$PATH:" == *":$bin_dir:"* ]]; then + pass "$bin_dir is in PATH" + else + warn "$bin_dir not in PATH" + info " Add to .zshrc: export PATH=\"\$HOME/.local/bin:\$PATH\"" + fi + else + warn "~/.local/bin directory doesn't exist" + fi +} + +print_summary() { + echo + echo -e "${BLUE}━━━ Summary ━━━${NC}" + echo + echo -e " ${GREEN}Passed:${NC} $PASS_COUNT" + echo -e " ${YELLOW}Warnings:${NC} $WARN_COUNT" + echo -e " ${RED}Failed:${NC} $FAIL_COUNT" + echo + + if [[ $FAIL_COUNT -eq 0 && $WARN_COUNT -eq 0 ]]; then + echo -e "${GREEN}✓ All checks passed! Your dotfiles are healthy.${NC}" + elif [[ $FAIL_COUNT -eq 0 ]]; then + echo -e "${YELLOW}⚠ Some warnings, but no critical issues.${NC}" + else + echo -e "${RED}✗ Some issues found.${NC}" + if [[ "$FIX_MODE" != true ]]; then + echo -e " Run with ${CYAN}--fix${NC} to attempt automatic fixes." + fi + fi + echo +} + +# ============================================================================ +# Main +# ============================================================================ + +main() { + print_header + + check_dotfiles_dir + check_symlinks + check_shell + check_zsh_plugins + check_git + check_espanso + check_optional_tools + check_bin_scripts + + print_summary + + # Exit with error code if there were failures + [[ $FAIL_COUNT -eq 0 ]] +} + +main "$@" diff --git a/bin/dotfiles-version.sh b/bin/dotfiles-version.sh new file mode 100644 index 0000000..2c05123 --- /dev/null +++ b/bin/dotfiles-version.sh @@ -0,0 +1,227 @@ +#!/usr/bin/env bash +# ============================================================================ +# Dotfiles Version Checker +# ============================================================================ +# Shows current and remote version info +# +# Usage: +# dotfiles-version.sh # Show version info +# dotfiles-version.sh --check # Check for updates (exit 1 if behind) +# ============================================================================ + +# ============================================================================ +# Load Configuration +# ============================================================================ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DOTFILES_CONF="${SCRIPT_DIR}/../dotfiles.conf" +[[ -f "$DOTFILES_CONF" ]] || DOTFILES_CONF="${SCRIPT_DIR}/dotfiles.conf" +[[ -f "$DOTFILES_CONF" ]] || DOTFILES_CONF="$HOME/.dotfiles/dotfiles.conf" + +if [[ -f "$DOTFILES_CONF" ]]; then + source "$DOTFILES_CONF" +else + DOTFILES_DIR="$HOME/.dotfiles" + DOTFILES_VERSION="unknown" + DOTFILES_BRANCH="main" + DOTFILES_RAW_URL="https://raw.githubusercontent.com/adlee-was-taken/dotfiles/main" +fi + +# ============================================================================ +# Colors +# ============================================================================ + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' + +# ============================================================================ +# Options +# ============================================================================ + +CHECK_ONLY=false + +for arg in "$@"; do + case "$arg" in + --check|-c) + CHECK_ONLY=true + ;; + --help|-h) + echo "Usage: $0 [OPTIONS]" + echo + echo "Options:" + echo " --check Only check for updates (exit 1 if behind)" + echo " --help Show this help message" + echo + exit 0 + ;; + esac +done + +# ============================================================================ +# Functions +# ============================================================================ + +get_local_version() { + echo "${DOTFILES_VERSION:-unknown}" +} + +get_local_commit() { + if [[ -d "$DOTFILES_DIR/.git" ]]; then + cd "$DOTFILES_DIR" + git rev-parse --short HEAD 2>/dev/null || echo "unknown" + cd - > /dev/null + else + echo "not a git repo" + fi +} + +get_local_date() { + if [[ -d "$DOTFILES_DIR/.git" ]]; then + cd "$DOTFILES_DIR" + git log -1 --format="%ci" 2>/dev/null | cut -d' ' -f1 || echo "unknown" + cd - > /dev/null + else + echo "unknown" + fi +} + +get_remote_version() { + # Try to get version from remote dotfiles.conf + local remote_conf=$(curl -fsSL "${DOTFILES_RAW_URL}/dotfiles.conf" 2>/dev/null) + if [[ -n "$remote_conf" ]]; then + echo "$remote_conf" | grep -oP 'DOTFILES_VERSION="\K[^"]+' || echo "unknown" + else + echo "unavailable" + fi +} + +get_remote_commit() { + if [[ -d "$DOTFILES_DIR/.git" ]]; then + cd "$DOTFILES_DIR" + git fetch origin --quiet 2>/dev/null || true + git rev-parse --short "origin/${DOTFILES_BRANCH}" 2>/dev/null || echo "unavailable" + cd - > /dev/null + else + echo "not a git repo" + fi +} + +get_commits_behind() { + if [[ -d "$DOTFILES_DIR/.git" ]]; then + cd "$DOTFILES_DIR" + git fetch origin --quiet 2>/dev/null || true + local behind=$(git rev-list HEAD.."origin/${DOTFILES_BRANCH}" --count 2>/dev/null) + echo "${behind:-0}" + cd - > /dev/null + else + echo "0" + fi +} + +compare_versions() { + local local_v="$1" + local remote_v="$2" + + if [[ "$local_v" == "unknown" || "$remote_v" == "unknown" || "$remote_v" == "unavailable" ]]; then + echo "unknown" + return + fi + + if [[ "$local_v" == "$remote_v" ]]; then + echo "current" + else + # Simple semver comparison + local local_parts=(${local_v//./ }) + local remote_parts=(${remote_v//./ }) + + for i in 0 1 2; do + local l=${local_parts[$i]:-0} + local r=${remote_parts[$i]:-0} + if (( l < r )); then + echo "behind" + return + elif (( l > r )); then + echo "ahead" + return + fi + done + echo "current" + fi +} + +# ============================================================================ +# Main +# ============================================================================ + +main() { + local local_version=$(get_local_version) + local local_commit=$(get_local_commit) + local local_date=$(get_local_date) + local remote_version=$(get_remote_version) + local remote_commit=$(get_remote_commit) + local commits_behind=$(get_commits_behind) + local version_status=$(compare_versions "$local_version" "$remote_version") + + if [[ "$CHECK_ONLY" == true ]]; then + if [[ "$commits_behind" -gt 0 ]]; then + echo "Updates available: $commits_behind commit(s) behind" + exit 1 + else + echo "Up to date" + exit 0 + fi + fi + + echo -e "\n${BLUE}╔════════════════════════════════════════════════════════════╗${NC}" + echo -e "${BLUE}║${NC} Dotfiles Version Info ${BLUE}║${NC}" + echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}\n" + + echo -e "${CYAN}Local:${NC}" + echo -e " Version: ${GREEN}${local_version}${NC}" + echo -e " Commit: ${local_commit}" + echo -e " Date: ${local_date}" + echo -e " Path: ${DOTFILES_DIR}" + echo + + echo -e "${CYAN}Remote:${NC}" + echo -e " Version: ${remote_version}" + echo -e " Commit: ${remote_commit}" + echo -e " Branch: ${DOTFILES_BRANCH}" + echo + + echo -e "${CYAN}Status:${NC}" + + case "$version_status" in + current) + echo -e " Version: ${GREEN}✓ Up to date${NC}" + ;; + behind) + echo -e " Version: ${YELLOW}⚠ New version available: ${remote_version}${NC}" + ;; + ahead) + echo -e " Version: ${CYAN}ℹ Local is ahead of remote${NC}" + ;; + *) + echo -e " Version: ${YELLOW}? Cannot determine${NC}" + ;; + esac + + if [[ "$commits_behind" -gt 0 ]]; then + echo -e " Commits: ${YELLOW}⚠ ${commits_behind} commit(s) behind${NC}" + echo + echo -e "${YELLOW}To update:${NC}" + echo " cd ~/.dotfiles && git pull && ./install.sh" + echo " # or" + echo " update-dotfiles.sh" + elif [[ "$commits_behind" == "0" ]]; then + echo -e " Commits: ${GREEN}✓ Up to date${NC}" + fi + + echo +} + +main "$@" diff --git a/docs/SETUP_GUIDE.md b/docs/SETUP_GUIDE.md index 446486f..8944754 100644 --- a/docs/SETUP_GUIDE.md +++ b/docs/SETUP_GUIDE.md @@ -30,12 +30,39 @@ cd ~/.dotfiles 2. Installs dependencies (git, curl, zsh) 3. Backs up existing configs to `~/.dotfiles_backup_YYYYMMDD_HHMMSS/` 4. Installs oh-my-zsh -5. Creates symlinks -6. Optionally installs espanso, fzf, bat, eza -7. Sets zsh as default shell +5. Installs zsh plugins (autosuggestions, syntax-highlighting) +6. Configures git (prompts for name/email if not in config) +7. Creates symlinks +8. Optionally installs espanso, fzf, bat, eza +9. Sets zsh as default shell + +### Install Options + +```bash +./install.sh # Full interactive install +./install.sh --skip-deps # Skip dependency check (for re-runs) +./install.sh --deps-only # Only install dependencies +./install.sh --uninstall # Remove symlinks, offer to restore backups +./install.sh --uninstall --purge # Also remove ~/.dotfiles +./install.sh --help # Show all options +``` ## Post-Install +### Verify Installation + +```bash +dotfiles-doctor.sh # Check health of installation +dotfiles-doctor.sh --fix # Attempt to fix issues +``` + +### Check Version + +```bash +dotfiles-version.sh # Show local vs remote version +dotfiles-version.sh --check # Exit 1 if updates available +``` + ### Personalize Espanso ```bash @@ -140,6 +167,54 @@ sudo ./bin/deploy-zshtheme-systemwide.sh --status Creates symlinks from each user's oh-my-zsh themes folder to `/usr/local/share/zsh/themes/adlee.zsh-theme`. +## Configuration + +### dotfiles.conf + +The main configuration file. Edit to customize your installation: + +```bash +# --- Version --- +DOTFILES_VERSION="1.0.0" + +# --- User Identity --- +USER_FULLNAME="Your Name" +USER_EMAIL="you@example.com" +USER_GITHUB="yourusername" + +# --- Git Configuration --- +GIT_USER_NAME="" # Falls back to USER_FULLNAME +GIT_USER_EMAIL="" # Falls back to USER_EMAIL +GIT_DEFAULT_BRANCH="main" +GIT_CREDENTIAL_HELPER="store" + +# --- Feature Toggles --- +INSTALL_DEPS="auto" # "auto", "true", "false", or "ask" +INSTALL_ZSH_PLUGINS="true" # Auto-install zsh plugins +INSTALL_ESPANSO="ask" +INSTALL_FZF="ask" +INSTALL_BAT="ask" +INSTALL_EZA="ask" +SET_ZSH_DEFAULT="ask" +``` + +### Git Identity + +The installer configures git automatically: + +1. Uses `GIT_USER_NAME` / `GIT_USER_EMAIL` from config +2. Falls back to `USER_FULLNAME` / `USER_EMAIL` +3. Prompts if both are empty + +To reconfigure git later: + +```bash +git config --global user.name "New Name" +git config --global user.email "new@email.com" +``` + +Or edit `dotfiles.conf` and re-run `./install.sh`. + ## Customization Tips ### Add Aliases @@ -172,46 +247,46 @@ typeset -g COLOR_BLUE='%{$FG[069]%}' ## Troubleshooting -### Theme Not Loading +### Run the Doctor ```bash -grep ZSH_THEME ~/.zshrc # Should show: ZSH_THEME="adlee" -source ~/.zshrc +dotfiles-doctor.sh # Diagnose issues +dotfiles-doctor.sh --fix # Auto-fix what's possible ``` -### Espanso Not Expanding +### Common Issues + +| Issue | Fix | +|-------|-----| +| Theme not loading | `dotfiles-doctor.sh --fix` | +| Zsh plugins missing | `./install.sh` (auto-installs) | +| Espanso not expanding | `espanso restart` | +| Git identity not set | Re-run `./install.sh` | +| Broken symlinks | `./install.sh` | + +### Manual Fixes ```bash -espanso status # Should show "running" -espanso restart -espanso log # Check for errors -``` +# Reinstall zsh plugins +git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions +git clone https://github.com/zsh-users/zsh-syntax-highlighting ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting -### Broken Symlinks - -```bash -# Find broken symlinks in home -find ~ -maxdepth 1 -type l -xtype l - -# Re-run installer -cd ~/.dotfiles && ./install.sh -``` - -### Permission Errors - -```bash +# Fix permissions chmod +x ~/.dotfiles/install.sh chmod +x ~/.dotfiles/bin/* ``` -## Security Notes - -- `.gitignore` excludes `.env`, `secrets/`, and `*.local` files -- Review `git/.gitconfig` before pushing (contains email) -- Personal espanso snippets may contain sensitive info - ## Uninstalling +### Quick Uninstall + +```bash +./install.sh --uninstall # Remove symlinks, offer backup restore +./install.sh --uninstall --purge # Also delete ~/.dotfiles +``` + +### Manual Uninstall + ```bash # Remove symlinks rm ~/.zshrc ~/.gitconfig ~/.vimrc ~/.tmux.conf diff --git a/dotfiles.conf b/dotfiles.conf index 4b9aa9e..282545e 100644 --- a/dotfiles.conf +++ b/dotfiles.conf @@ -5,6 +5,9 @@ # All scripts source this file for configuration. # ============================================================================ +# --- Version --- +DOTFILES_VERSION="1.0.0" + # --- GitHub Settings --- DOTFILES_GITHUB_USER="adlee-was-taken" DOTFILES_REPO_NAME="dotfiles" @@ -14,7 +17,8 @@ DOTFILES_BRANCH="main" DOTFILES_DIR="$HOME/.dotfiles" DOTFILES_BACKUP_PREFIX="$HOME/.dotfiles_backup" -# --- User Identity (used by setup-espanso.sh) --- +# --- User Identity --- +# Used by setup-espanso.sh and git config generation # Leave blank to be prompted during setup, or pre-fill for automated installs USER_FULLNAME="" USER_EMAIL="" @@ -22,10 +26,18 @@ USER_PHONE="" USER_WEBSITE="" USER_GITHUB="" +# --- Git Configuration --- +# If blank, falls back to USER_FULLNAME/USER_EMAIL above +GIT_USER_NAME="" +GIT_USER_EMAIL="" +GIT_DEFAULT_BRANCH="master" +GIT_CREDENTIAL_HELPER="store" # "store", "cache", "osxkeychain", etc. + # --- Feature Toggles --- # Set to "true", "false", or "ask" to control what gets installed # Use "auto" to skip if already installed (default for deps) INSTALL_DEPS="auto" # "auto" (skip if installed), "true", "false", or "ask" +INSTALL_ZSH_PLUGINS="true" # Auto-install zsh-autosuggestions & zsh-syntax-highlighting INSTALL_ESPANSO="ask" # "true", "false", or "ask" INSTALL_FZF="ask" INSTALL_BAT="ask" @@ -38,7 +50,7 @@ THEME_TIMER_THRESHOLD=10 # Show elapsed time for commands longer than THEME_PATH_TRUNCATE_LENGTH=32 # Truncate path display after N characters # --- Espanso Settings --- -ESPANSO_TRIGGER_PREFIX=".." # Prefix for all Aaron D. Lee's normal triggers (e.g., "..date") +ESPANSO_TRIGGER_PREFIX=".." # Prefix for all triggers (e.g., "..date") # --- Snapper Settings (CachyOS/Arch with btrfs) --- SNAPPER_CONFIG="root" diff --git a/install.sh b/install.sh index 91d973c..e7960b6 100755 --- a/install.sh +++ b/install.sh @@ -10,6 +10,7 @@ # Options: # --skip-deps Skip dependency installation (for re-runs) # --deps-only Only install dependencies, then exit +# --uninstall Remove symlinks and optionally restore backups # --help Show help # # Fork this repo? Edit dotfiles.conf with your settings. @@ -23,6 +24,8 @@ set -e SKIP_DEPS=false DEPS_ONLY=false +UNINSTALL=false +UNINSTALL_PURGE=false for arg in "$@"; do case "$arg" in @@ -32,18 +35,32 @@ for arg in "$@"; do --deps-only) DEPS_ONLY=true ;; + --uninstall) + UNINSTALL=true + ;; + --purge) + UNINSTALL_PURGE=true + ;; --help|-h) echo "Usage: $0 [OPTIONS]" echo echo "Options:" echo " --skip-deps Skip dependency installation (useful for re-runs)" echo " --deps-only Only install dependencies, then exit" + echo " --uninstall Remove symlinks and restore backups" + echo " --purge With --uninstall, also remove ~/.dotfiles directory" echo " --help Show this help message" echo echo "Configuration:" echo " Edit dotfiles.conf to customize installation behavior" echo " Set INSTALL_DEPS=\"false\" to always skip dependencies" echo + echo "Examples:" + echo " ./install.sh # Full install" + echo " ./install.sh --skip-deps # Re-run without checking deps" + echo " ./install.sh --uninstall # Remove symlinks" + echo " ./install.sh --uninstall --purge # Remove everything" + echo exit 0 ;; esac @@ -61,6 +78,7 @@ load_config() { source "$conf_file" else # Fallback defaults for curl|bash install (before clone) + DOTFILES_VERSION="${DOTFILES_VERSION:-1.0.0}" DOTFILES_GITHUB_USER="${DOTFILES_GITHUB_USER:-adlee-was-taken}" DOTFILES_REPO_NAME="${DOTFILES_REPO_NAME:-dotfiles}" DOTFILES_BRANCH="${DOTFILES_BRANCH:-main}" @@ -70,6 +88,7 @@ load_config() { # Feature toggles INSTALL_DEPS="${INSTALL_DEPS:-auto}" + INSTALL_ZSH_PLUGINS="${INSTALL_ZSH_PLUGINS:-true}" INSTALL_ESPANSO="${INSTALL_ESPANSO:-ask}" INSTALL_FZF="${INSTALL_FZF:-ask}" INSTALL_BAT="${INSTALL_BAT:-ask}" @@ -78,6 +97,12 @@ load_config() { # Theme settings ZSH_THEME_NAME="${ZSH_THEME_NAME:-adlee}" + + # Git settings + GIT_USER_NAME="${GIT_USER_NAME:-}" + GIT_USER_EMAIL="${GIT_USER_EMAIL:-}" + GIT_DEFAULT_BRANCH="${GIT_DEFAULT_BRANCH:-master}" + GIT_CREDENTIAL_HELPER="${GIT_CREDENTIAL_HELPER:-store}" fi } @@ -93,6 +118,7 @@ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' +CYAN='\033[0;36m' NC='\033[0m' # ============================================================================ @@ -101,7 +127,7 @@ NC='\033[0m' print_header() { echo -e "\n${BLUE}╔════════════════════════════════════════════════════════════╗${NC}" - echo -e "${BLUE}║${NC} Dotfiles Installation ${BLUE}║${NC}" + echo -e "${BLUE}║${NC} Dotfiles Installation ${CYAN}v${DOTFILES_VERSION}${NC} ${BLUE}║${NC}" echo -e "${BLUE}║${NC} Repo: ${DOTFILES_GITHUB_USER}/${DOTFILES_REPO_NAME} ${BLUE}║${NC}" echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}\n" } @@ -156,6 +182,97 @@ should_install() { esac } +# ============================================================================ +# Uninstall Function +# ============================================================================ + +do_uninstall() { + echo -e "\n${BLUE}╔════════════════════════════════════════════════════════════╗${NC}" + echo -e "${BLUE}║${NC} Dotfiles Uninstallation ${BLUE}║${NC}" + echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}\n" + + print_step "Removing symlinks" + + local symlinks=( + "$HOME/.zshrc" + "$HOME/.gitconfig" + "$HOME/.vimrc" + "$HOME/.tmux.conf" + "$HOME/.oh-my-zsh/themes/${ZSH_THEME_NAME:-adlee}.zsh-theme" + "$HOME/.config/espanso" + ) + + for link in "${symlinks[@]}"; do + if [[ -L "$link" ]]; then + rm "$link" + print_success "Removed: $link" + elif [[ -e "$link" ]]; then + print_warning "Not a symlink (skipped): $link" + fi + done + + # Remove bin symlinks + if [[ -d "$HOME/.local/bin" ]]; then + for script in "$HOME/.local/bin"/*; do + if [[ -L "$script" ]] && [[ "$(readlink "$script")" == *".dotfiles"* ]]; then + rm "$script" + print_success "Removed: $script" + fi + done + fi + + # Find and offer to restore backups + print_step "Looking for backups" + + local backup_dirs=($(ls -d ${DOTFILES_BACKUP_PREFIX}_* 2>/dev/null || true)) + + if [[ ${#backup_dirs[@]} -gt 0 ]]; then + echo "Found ${#backup_dirs[@]} backup(s):" + for i in "${!backup_dirs[@]}"; do + echo " $((i+1)). ${backup_dirs[$i]}" + done + echo + + if ask_yes_no "Restore from most recent backup?"; then + local latest_backup="${backup_dirs[-1]}" + print_step "Restoring from: $latest_backup" + + for file in "$latest_backup"/*; do + if [[ -f "$file" ]]; then + local filename=$(basename "$file") + cp "$file" "$HOME/.$filename" 2>/dev/null || cp "$file" "$HOME/$filename" + print_success "Restored: $filename" + fi + done + fi + else + print_warning "No backups found" + fi + + # Purge dotfiles directory if requested + if [[ "$UNINSTALL_PURGE" == true ]]; then + print_step "Purging dotfiles directory" + + if [[ -d "$DOTFILES_DIR" ]]; then + if ask_yes_no "Delete $DOTFILES_DIR?" "n"; then + rm -rf "$DOTFILES_DIR" + print_success "Removed: $DOTFILES_DIR" + else + print_warning "Kept: $DOTFILES_DIR" + fi + fi + fi + + echo + print_success "Uninstallation complete!" + echo + echo "You may also want to:" + echo " - Remove oh-my-zsh: rm -rf ~/.oh-my-zsh" + echo " - Change shell back: chsh -s /bin/bash" + echo + exit 0 +} + # ============================================================================ # Installation Functions # ============================================================================ @@ -293,6 +410,89 @@ install_oh_my_zsh() { fi } +install_zsh_plugins() { + print_step "Installing zsh plugins" + + local custom_dir="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins" + mkdir -p "$custom_dir" + + # zsh-autosuggestions + if [[ ! -d "$custom_dir/zsh-autosuggestions" ]]; then + git clone --depth 1 https://github.com/zsh-users/zsh-autosuggestions "$custom_dir/zsh-autosuggestions" + print_success "Installed: zsh-autosuggestions" + else + print_success "Already installed: zsh-autosuggestions" + fi + + # zsh-syntax-highlighting + if [[ ! -d "$custom_dir/zsh-syntax-highlighting" ]]; then + git clone --depth 1 https://github.com/zsh-users/zsh-syntax-highlighting "$custom_dir/zsh-syntax-highlighting" + print_success "Installed: zsh-syntax-highlighting" + else + print_success "Already installed: zsh-syntax-highlighting" + fi +} + +configure_git() { + print_step "Configuring git" + + # Determine git user info (config > user identity > prompt) + local git_name="${GIT_USER_NAME:-$USER_FULLNAME}" + local git_email="${GIT_USER_EMAIL:-$USER_EMAIL}" + + # Prompt if still empty + if [[ -z "$git_name" ]]; then + local current_name=$(git config --global user.name 2>/dev/null || echo "") + if [[ -n "$current_name" ]]; then + print_success "Git name already set: $current_name" + else + read -p "Git user name: " git_name + fi + fi + + if [[ -z "$git_email" ]]; then + local current_email=$(git config --global user.email 2>/dev/null || echo "") + if [[ -n "$current_email" ]]; then + print_success "Git email already set: $current_email" + else + read -p "Git email: " git_email + fi + fi + + # Generate .gitconfig + local gitconfig_path="$DOTFILES_DIR/git/.gitconfig" + mkdir -p "$DOTFILES_DIR/git" + + cat > "$gitconfig_path" << EOF +[init] + defaultBranch = ${GIT_DEFAULT_BRANCH:-master} +[user] + email = ${git_email} + name = ${git_name} +[credential] + helper = ${GIT_CREDENTIAL_HELPER:-store} +[core] + editor = vim + autocrlf = input +[pull] + rebase = false +[push] + default = current +[alias] + st = status + co = checkout + br = branch + ci = commit + lg = log --oneline --graph --decorate --all +EOF + + print_success "Generated: .gitconfig" + + # Also set git config directly (in case symlink isn't in place yet) + [[ -n "$git_name" ]] && git config --global user.name "$git_name" + [[ -n "$git_email" ]] && git config --global user.email "$git_email" +} + link_dotfiles() { print_step "Linking dotfiles" @@ -502,6 +702,12 @@ install_optional_tools() { # ============================================================================ main() { + # Handle uninstall mode + if [[ "$UNINSTALL" == true ]]; then + load_config + do_uninstall + fi + print_header detect_os @@ -519,6 +725,13 @@ main() { clone_or_update_dotfiles backup_existing_configs install_oh_my_zsh + + # Install zsh plugins if enabled + if [[ "${INSTALL_ZSH_PLUGINS}" == "true" || "${INSTALL_ZSH_PLUGINS}" == "yes" || "${INSTALL_ZSH_PLUGINS}" == "1" ]]; then + install_zsh_plugins + fi + + configure_git link_dotfiles link_espanso_config set_zsh_default @@ -531,10 +744,14 @@ main() { echo " 1. Restart your terminal or run: exec zsh" echo " 2. Your old configs are backed up in: $BACKUP_DIR" echo " 3. Customize settings in: $DOTFILES_DIR/dotfiles.conf" + echo " 4. Run 'dotfiles-doctor.sh' to verify installation" echo echo -e "${BLUE}To update dotfiles in the future:${NC}" echo " cd ~/.dotfiles && git pull && ./install.sh" echo + echo -e "${BLUE}To uninstall:${NC}" + echo " ./install.sh --uninstall" + echo else print_warning "Installation cancelled" exit 0