diff --git a/README.md b/README.md index 01dee29..d8b080c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,82 @@ # Ryan's Dotfiles -Minimal Bash dotfiles for SSH and remote environments. +A comprehensive dotfiles repository for SSH, remote environments, and development setups. + +## Structure + +``` +~/.dotfiles/ +├── stow/ # Configs organized for GNU Stow +│ ├── bash/ # Bash configuration (.bashrc, .bash_aliases, .inputrc) +│ ├── git/ # Git configuration (.gitconfig) +│ └── [other configs] # Additional configurations as needed +├── packages/ +│ ├── base.txt # MUST HAVE - Core system utilities +│ ├── cli-tools.txt # Nice-to-haves - Enhanced CLI tools +│ ├── dev.txt # Development tools (docker, node, etc) +│ └── gui.txt # Desktop applications +├── bootstrap.sh # Get repo and select branch +├── setup.sh # Main orchestrator - runs all configuration +└── README.md # Documentation +``` ## Install +**From internet:** + ```bash bash <(curl -sL https://ryans.tools/dotfiles) +``` + +**From local copy (USB drive, etc.):** + +```bash +./bootstrap.sh +``` + +The installer will: + +1. **Bootstrap:** Clone repository (or use local copy), select branch if desired +2. **Setup:** Check dependencies, backup conflicts, install packages, configure with GNU Stow + +## Updates + +**Regular updates (recommended):** + +```bash +dotpull +``` + +**Branch switching or major re-setup:** + +```bash +cd ~/.dotfiles && ./bootstrap.sh +``` + +## Package Categories + +- **Base**: Essential system utilities (curl, git, stow, vim, etc.) +- **CLI Tools**: Enhanced command-line tools (bat, fzf, ripgrep, etc.) +- **Development**: Programming languages and dev tools (nodejs, python, docker, etc.) +- **GUI**: Desktop applications (firefox, code, vlc, etc.) + +## Updating + +After initial installation, use the `dotpull` alias to update your dotfiles: + +```bash +dotpull +``` + +This alias will pull the latest changes and re-stow all configurations. + +## Manual Management + +To manage individual configurations: + +```bash +cd ~/.dotfiles/stow +stow -t ~ bash # Link bash config +stow -t ~ git # Link git config +stow -D bash # Unlink bash config +``` diff --git a/bootstrap.sh b/bootstrap.sh new file mode 100644 index 0000000..0aabd52 --- /dev/null +++ b/bootstrap.sh @@ -0,0 +1,71 @@ +#!/bin/bash +set -euo pipefail + +DOTFILES_REPO="https://gitea.purpleraft.com/ryan/dotfiles" +DOTFILES_DIR="$HOME/.dotfiles" + +echo "Starting dotfiles bootstrap..." + +# Clone repo (new installs) or update for branch switching +if [ -d "$DOTFILES_DIR/.git" ]; then + echo "Updating existing dotfiles repo..." + git -C "$DOTFILES_DIR" pull --quiet +else + echo "Cloning dotfiles into $DOTFILES_DIR..." + git clone "$DOTFILES_REPO" "$DOTFILES_DIR" +fi + +cd "$DOTFILES_DIR" + +# Branch selection - allows choosing which version of dotfiles to install +# - New installs: git clone defaults to 'main', but you can switch to any branch +# - Existing installs: switch from current branch to a different one +# - Only runs in interactive terminals (skipped in automated/CI environments) +if [ -t 0 ] && [ -t 1 ] && [ -d ".git" ]; then + current_branch=$(git branch --show-current 2>/dev/null || echo "unknown") + echo "Current branch: $current_branch" + + echo + read -p "Switch to a different branch? (y/N): " -n 1 -r + echo + + if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "Available branches:" + git branch -a --format="%(refname:short)" | grep -v "HEAD" | sed 's|^origin/||' | sort -u | nl -w2 -s'. ' + + branches=($(git branch -a --format="%(refname:short)" | grep -v "HEAD" | sed 's|^origin/||' | sort -u)) + + while true; do + read -p "Enter branch number (1-${#branches[@]}): " branch_num + + if [[ "$branch_num" =~ ^[0-9]+$ ]] && [ "$branch_num" -ge 1 ] && [ "$branch_num" -le "${#branches[@]}" ]; then + selected_branch="${branches[$((branch_num-1))]}" + echo "Switching to branch: $selected_branch" + + if git show-ref --verify --quiet "refs/heads/$selected_branch"; then + git checkout "$selected_branch" + elif git show-ref --verify --quiet "refs/remotes/origin/$selected_branch"; then + git checkout -b "$selected_branch" "origin/$selected_branch" + else + echo "Error: Branch $selected_branch not found" + exit 1 + fi + + git pull --quiet || true + echo "Switched to branch: $selected_branch" + break + else + echo "Invalid selection. Please enter a number between 1 and ${#branches[@]}" + fi + done + fi +fi + +# Hand off to setup +if [ -f "setup.sh" ]; then + echo "Starting setup..." + exec "./setup.sh" +else + echo "Error: No setup.sh found!" + exit 1 +fi diff --git a/install.sh b/install.sh deleted file mode 100644 index 6546502..0000000 --- a/install.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/bash -set -euo pipefail - -DOTFILES_REPO="https://gitea.purpleraft.com/ryan/dotfiles" -DOTFILES_DIR="$HOME/.dotfiles" - -# Source utilities if available -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -if [ -f "$SCRIPT_DIR/scripts/utils.sh" ]; then - source "$SCRIPT_DIR/scripts/utils.sh" -else - # Fallback logging functions if utils not available - log_info() { echo "ℹ️ $1"; } - log_success() { echo "✅ $1"; } - log_warning() { echo "⚠️ $1"; } - log_error() { echo "❌ $1"; } -fi - -log_info "🚀 Starting dotfiles installation..." - -# Clone or update the repo -if [ -d "$DOTFILES_DIR/.git" ]; then - log_info "Updating existing dotfiles repo..." - git -C "$DOTFILES_DIR" pull --quiet -else - log_info "Cloning dotfiles into $DOTFILES_DIR..." - git clone "$DOTFILES_REPO" "$DOTFILES_DIR" -fi - -cd "$DOTFILES_DIR" - -# Auto-discover and run setup scripts -if [ -d "scripts" ]; then - log_info "Running setup scripts..." - - # Look for numbered scripts and run them in order - for script in scripts/[0-9][0-9]-*.sh; do - if [ -f "$script" ]; then - # Make executable and run - chmod +x "$script" 2>/dev/null || true - script_name=$(basename "$script") - log_info "Running $script_name..." - "$script" "$DOTFILES_DIR" - fi - done -else - log_error "No scripts directory found! Something went wrong with the installation." - exit 1 -fi - -log_success "Dotfiles install complete!" diff --git a/scripts/05-branch-selection.sh b/scripts/05-branch-selection.sh new file mode 100644 index 0000000..a873513 --- /dev/null +++ b/scripts/05-branch-selection.sh @@ -0,0 +1,100 @@ +#!/bin/bash +# scripts/05-branch-selection.sh - Allow user to select git branch + +set -euo pipefail + +# Source utilities +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/utils.sh" + +select_branch() { + local dotfiles_dir="$1" + + # Change to dotfiles directory + cd "$dotfiles_dir" + + # Get current branch + local current_branch + current_branch=$(git branch --show-current 2>/dev/null || echo "unknown") + + log_info "Current branch: $current_branch" + + # Only prompt if we're in an interactive terminal + if ! is_interactive; then + log_info "Non-interactive mode, staying on current branch: $current_branch" + return 0 + fi + + # Get available branches (both local and remote) + log_info "Available branches:" + git branch -a --format="%(refname:short)" | grep -v "HEAD" | sort -u | nl -w2 -s'. ' + + echo + read -p "Switch to a different branch? (y/N): " -n 1 -r + echo + + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + log_info "Staying on current branch: $current_branch" + return 0 + fi + + # Get list of branches for selection + local branches=() + while IFS= read -r branch; do + # Clean up branch names (remove origin/ prefix, etc.) + clean_branch=$(echo "$branch" | sed 's|^origin/||' | sed 's|^remotes/origin/||') + # Skip HEAD and duplicates + if [[ "$clean_branch" != "HEAD" ]] && [[ ! " ${branches[@]} " =~ " ${clean_branch} " ]]; then + branches+=("$clean_branch") + fi + done < <(git branch -a --format="%(refname:short)" | grep -v "HEAD") + + # Sort branches + IFS=$'\n' branches=($(sort <<<"${branches[*]}")) + + echo "Select a branch:" + for i in "${!branches[@]}"; do + echo "$((i+1)). ${branches[i]}" + done + echo + + while true; do + read -p "Enter branch number (1-${#branches[@]}): " branch_num + + if [[ "$branch_num" =~ ^[0-9]+$ ]] && [ "$branch_num" -ge 1 ] && [ "$branch_num" -le "${#branches[@]}" ]; then + selected_branch="${branches[$((branch_num-1))]}" + break + else + log_warning "Invalid selection. Please enter a number between 1 and ${#branches[@]}" + fi + done + + # Switch to selected branch + log_info "Switching to branch: $selected_branch" + + if git show-ref --verify --quiet "refs/heads/$selected_branch"; then + # Local branch exists + git checkout "$selected_branch" + elif git show-ref --verify --quiet "refs/remotes/origin/$selected_branch"; then + # Remote branch exists, create local tracking branch + git checkout -b "$selected_branch" "origin/$selected_branch" + else + log_error "Branch $selected_branch not found" + return 1 + fi + + # Pull latest changes + log_info "Pulling latest changes..." + git pull --quiet || true + + log_success "Switched to branch: $selected_branch" +} + +# Run if called directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + if [ $# -ne 1 ]; then + log_error "Usage: $0 " + exit 1 + fi + select_branch "$1" +fi diff --git a/scripts/10-backup-files.sh b/scripts/10-backup-files.sh index 84ef4f1..52b1c78 100644 --- a/scripts/10-backup-files.sh +++ b/scripts/10-backup-files.sh @@ -12,17 +12,41 @@ backup_files() { log_info "Checking for file conflicts..." - # Files that might conflict with stow - local files_to_check=(.bashrc .bash_aliases .inputrc .gitconfig) local backed_up=() - for file in "${files_to_check[@]}"; do - if [ -f "$HOME/$file" ] && [ ! -L "$HOME/$file" ]; then - log_warning "Backing up existing file: $HOME/$file -> $HOME/${file}.bak" - mv "$HOME/$file" "$HOME/${file}.bak" - backed_up+=("$file") - fi - done + # Auto-discover files from stow directories + if [ -d "$dotfiles_dir/stow" ]; then + # Use new stow structure - scan all files in stow subdirectories + for config_dir in "$dotfiles_dir/stow"/*; do + if [ -d "$config_dir" ]; then + log_info "Checking $(basename "$config_dir") configuration for conflicts..." + + # Find all files that would be stowed + find "$config_dir" -type f | while read -r file; do + # Get relative path from config directory + relative_file="${file#$config_dir/}" + target_file="$HOME/$relative_file" + + if [ -f "$target_file" ] && [ ! -L "$target_file" ]; then + log_warning "Backing up existing file: $target_file -> ${target_file}.bak" + mv "$target_file" "${target_file}.bak" + backed_up+=("$relative_file") + fi + done + fi + done + else + # Fallback to old structure - hardcoded file list + local files_to_check=(.bashrc .bash_aliases .inputrc .gitconfig) + + for file in "${files_to_check[@]}"; do + if [ -f "$HOME/$file" ] && [ ! -L "$HOME/$file" ]; then + log_warning "Backing up existing file: $HOME/$file -> $HOME/${file}.bak" + mv "$HOME/$file" "$HOME/${file}.bak" + backed_up+=("$file") + fi + done + fi if [ ${#backed_up[@]} -gt 0 ]; then log_info "Backed up ${#backed_up[@]} file(s): ${backed_up[*]}" diff --git a/scripts/20-setup-stow.sh b/scripts/20-setup-stow.sh index 3fbe526..cc1afb0 100644 --- a/scripts/20-setup-stow.sh +++ b/scripts/20-setup-stow.sh @@ -10,16 +10,43 @@ source "$SCRIPT_DIR/utils.sh" setup_stow() { local dotfiles_dir="$1" - # Change to dotfiles directory and use stow to create symlinks - cd "$dotfiles_dir" - log_info "Using Stow to symlink dotfiles..." - - if ! stow --adopt -t "$HOME" . 2>/dev/null; then - log_warning "Adopting failed, trying regular stow..." - stow -t "$HOME" . + if [ -d "$dotfiles_dir/stow" ]; then + # Use new stow structure - stow each subdirectory individually + cd "$dotfiles_dir/stow" + log_info "Using Stow to symlink dotfiles from stow/ subdirectories..." + + local stowed=() + + for config_dir in *; do + if [ -d "$config_dir" ]; then + log_info "Stowing $config_dir configuration..." + + if ! stow --adopt -t "$HOME" "$config_dir" 2>/dev/null; then + log_warning "Adopting failed for $config_dir, trying regular stow..." + stow -t "$HOME" "$config_dir" + fi + + stowed+=("$config_dir") + fi + done + + if [ ${#stowed[@]} -gt 0 ]; then + log_success "Stowed ${#stowed[@]} configuration(s): ${stowed[*]}" + else + log_warning "No stow subdirectories found in $dotfiles_dir/stow" + fi + else + # Fallback to old structure - stow entire directory + cd "$dotfiles_dir" + log_info "Using Stow to symlink dotfiles from root directory..." + + if ! stow --adopt -t "$HOME" . 2>/dev/null; then + log_warning "Adopting failed, trying regular stow..." + stow -t "$HOME" . + fi + + log_success "Stow setup complete" fi - - log_success "Stow setup complete" } # Run if called directly diff --git a/scripts/30-install-packages.sh b/scripts/30-install-packages.sh new file mode 100644 index 0000000..3670279 --- /dev/null +++ b/scripts/30-install-packages.sh @@ -0,0 +1,125 @@ +#!/bin/bash +# scripts/30-install-packages.sh - Install packages from package lists + +set -euo pipefail + +# Source utilities +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/utils.sh" + +# Detect package manager +detect_package_manager() { + if command_exists apt; then + echo "apt" + elif command_exists yum; then + echo "yum" + elif command_exists dnf; then + echo "dnf" + elif command_exists pacman; then + echo "pacman" + elif command_exists brew; then + echo "brew" + else + echo "unknown" + fi +} + +# Install packages with detected package manager +install_packages() { + local package_manager="$1" + shift + local packages=("$@") + + if [ ${#packages[@]} -eq 0 ]; then + log_info "No packages to install" + return 0 + fi + + case "$package_manager" in + apt) + log_info "Installing packages with apt..." + sudo apt update && sudo apt install -y "${packages[@]}" + ;; + yum) + log_info "Installing packages with yum..." + sudo yum install -y "${packages[@]}" + ;; + dnf) + log_info "Installing packages with dnf..." + sudo dnf install -y "${packages[@]}" + ;; + pacman) + log_info "Installing packages with pacman..." + sudo pacman -S --noconfirm "${packages[@]}" + ;; + brew) + log_info "Installing packages with brew..." + brew install "${packages[@]}" + ;; + *) + log_error "Unknown package manager. Please install packages manually." + return 1 + ;; + esac +} + +# Read packages from file +read_package_file() { + local file="$1" + if [ -f "$file" ]; then + # Filter out comments and empty lines + grep -v '^#' "$file" | grep -v '^[[:space:]]*$' | tr '\n' ' ' + fi +} + +install_package_lists() { + local dotfiles_dir="$1" + local packages_dir="$dotfiles_dir/packages" + + if [ ! -d "$packages_dir" ]; then + log_warning "No packages directory found at $packages_dir" + return 0 + fi + + local package_manager + package_manager=$(detect_package_manager) + + if [ "$package_manager" = "unknown" ]; then + log_error "No supported package manager found" + return 1 + fi + + log_info "Detected package manager: $package_manager" + + # Install package lists in order + local package_files=("base.txt" "cli-tools.txt" "dev.txt" "gui.txt") + + for package_file in "${package_files[@]}"; do + local file_path="$packages_dir/$package_file" + if [ -f "$file_path" ]; then + log_info "Installing packages from $package_file..." + local packages + packages=$(read_package_file "$file_path") + + if [ -n "$packages" ]; then + # Convert space-separated string to array + read -ra package_array <<< "$packages" + install_packages "$package_manager" "${package_array[@]}" + log_success "Completed installation from $package_file" + else + log_info "No packages found in $package_file" + fi + else + log_info "Package file $package_file not found, skipping" + fi + done +} + +# Run if called directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + if [ $# -ne 1 ]; then + log_error "Usage: $0 " + exit 1 + fi + install_package_lists "$1" +fi diff --git a/scripts/utils.sh b/scripts/utils.sh index 465c702..0860494 100644 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -34,3 +34,49 @@ command_exists() { is_interactive() { [[ $- == *i* ]] } + +# Branch selection utilities +list_branches() { + local dotfiles_dir="$1" + cd "$dotfiles_dir" + + # Get available branches (both local and remote) + local branches=() + while IFS= read -r branch; do + # Clean up branch names (remove origin/ prefix, etc.) + clean_branch=$(echo "$branch" | sed 's|^origin/||' | sed 's|^remotes/origin/||') + # Skip HEAD and duplicates + if [[ "$clean_branch" != "HEAD" ]] && [[ ! " ${branches[@]} " =~ " ${clean_branch} " ]]; then + branches+=("$clean_branch") + fi + done < <(git branch -a --format="%(refname:short)" | grep -v "HEAD") + + # Sort and return branches + printf '%s\n' "${branches[@]}" | sort +} + +switch_to_branch() { + local dotfiles_dir="$1" + local selected_branch="$2" + + cd "$dotfiles_dir" + + log_info "Switching to branch: $selected_branch" + + if git show-ref --verify --quiet "refs/heads/$selected_branch"; then + # Local branch exists + git checkout "$selected_branch" + elif git show-ref --verify --quiet "refs/remotes/origin/$selected_branch"; then + # Remote branch exists, create local tracking branch + git checkout -b "$selected_branch" "origin/$selected_branch" + else + log_error "Branch $selected_branch not found" + return 1 + fi + + # Pull latest changes + log_info "Pulling latest changes..." + git pull --quiet || true + + log_success "Switched to branch: $selected_branch" +} diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..f141a6b --- /dev/null +++ b/setup.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# setup.sh - Main dotfiles setup orchestrator +# Runs all numbered scripts in sequence + +set -euo pipefail + +# Get the absolute path to the directory containing this script +# This handles symlinks and relative paths to always find the dotfiles root +DOTFILES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source utilities with fallback +if [ -f "$DOTFILES_DIR/scripts/utils.sh" ]; then + source "$DOTFILES_DIR/scripts/utils.sh" +else + echo "Error: scripts/utils.sh not found! Repository structure is corrupted." + exit 1 +fi + +log_info "🔧 Starting dotfiles setup..." +log_info "Working directory: $DOTFILES_DIR" + +# Auto-discover and run setup scripts +if [ -d "$DOTFILES_DIR/scripts" ]; then + log_info "Running setup scripts..." + + # Look for numbered scripts and run them in order + script_count=0 + for script in "$DOTFILES_DIR/scripts"/[0-9][0-9]-*.sh; do + if [ -f "$script" ]; then + # Make executable and run + chmod +x "$script" 2>/dev/null || true + script_name=$(basename "$script") + log_info "Running $script_name..." + + if "$script" "$DOTFILES_DIR"; then + log_success "✓ $script_name completed" + else + log_error "✗ $script_name failed" + exit 1 + fi + + ((script_count++)) + fi + done + + if [ $script_count -eq 0 ]; then + log_warning "No numbered scripts found in scripts/ directory" + else + log_success "Completed $script_count setup script(s)" + fi +else + log_error "No scripts directory found! Something went wrong with the installation." + exit 1 +fi + +log_success "🎉 Dotfiles setup complete!"