diff options
| -rwxr-xr-x | linux/home/install.sh | 2642 |
1 files changed, 1934 insertions, 708 deletions
diff --git a/linux/home/install.sh b/linux/home/install.sh index 1b52e27..413edd7 100755 --- a/linux/home/install.sh +++ b/linux/home/install.sh @@ -1,927 +1,2153 @@ #!/usr/bin/env bash +# Dotfiles Installation Script +#========================================================= + +set -euo pipefail # Exit on error, undefined vars, pipe failures + #====================================== -# Variables +# Variables & Configuration #====================================== -# Color definitions +# Color definitions for pretty UI NOCOLOR='\033[0m' -RED='\033[00;31m' -GREEN='\033[00;32m' +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +MAGENTA='\033[0;35m' +CYAN='\033[0;36m' +WHITE='\033[0;37m' +BOLD='\033[1m' + +# Dotfiles configuration +DOTFILES_URL='https://github.com/srdusr/dotfiles.git' +DOTFILES_DIR="$HOME/.cfg" +LOG_FILE="$HOME/.local/share/dotfiles_install.log" +TRASH_DIR="$HOME/.local/share/Trash" +STATE_FILE="$HOME/.local/share/dotfiles_install_state" +BACKUP_DIR="$HOME/.dotfiles-backup-$(date +%Y%m%d-%H%M%S)" + +# Installation tracking +INSTALL_SUMMARY=() +FAILED_ITEMS=() +SKIPPED_ITEMS=() +COMPLETED_STEPS=() + +# Script options +RESUME_MODE=false +UPDATE_MODE=false +VERBOSE_MODE=false +DRY_RUN=false +FORCE_MODE=false + +# Installation steps configuration +declare -A INSTALLATION_STEPS=( + ["install_dotfiles"]="Install dotfiles repository" + ["setup_user_dirs"]="Setup user directories" + ["install_packages"]="Install system packages" + ["setup_shell"]="Setup shell environment" + ["setup_ssh"]="Setup SSH configuration" + ["configure_services"]="Configure system services" + ["setup_development"]="Setup development environment" + ["apply_tweaks"]="Apply system tweaks" +) + +# Step order (important for dependencies) +STEP_ORDER=( + "install_dotfiles" + "setup_user_dirs" + "install_packages" + "setup_shell" + "setup_ssh" + "configure_services" + "setup_development" + "apply_tweaks" +) + +#====================================== +# State Management Functions +#====================================== -# Dotfiles -dotfiles_url='https://github.com/srdusr/dotfiles.git' -dotfiles_dir="$HOME/.cfg" +# Save current state +save_state() { + local current_step="$1" + local status="$2" # started, completed, failed -# Log file -LOG_FILE="dotfiles.log" -TRASH_DIR="$HOME/.local/share/Trash" + mkdir -p "$(dirname "$STATE_FILE")" -# Ensure Trash directory exists -if [ ! -d "$TRASH_DIR" ]; then - mkdir -p "$TRASH_DIR" - if [ $? -ne 0 ]; then - echo "Failed to create Trash directory. Exiting..." - exit 1 + { + echo "LAST_STEP=$current_step" + echo "STEP_STATUS=$status" + echo "TIMESTAMP=$(date +%s)" + echo "RESUME_AVAILABLE=true" + + # Save completed steps + echo "COMPLETED_STEPS=(${COMPLETED_STEPS[*]})" + + # Save environment info + echo "CFG_OS=$CFG_OS" + echo "DISTRO=${DISTRO:-}" + echo "PRIVILEGE_TOOL=${PRIVILEGE_TOOL:-}" + + } > "$STATE_FILE" +} + +# Load previous state +load_state() { + if [[ -f "$STATE_FILE" ]]; then + source "$STATE_FILE" + return 0 + else + return 1 fi -fi +} + +# Clear state file +clear_state() { + [[ -f "$STATE_FILE" ]] && rm -f "$STATE_FILE" +} + +# Check if step was completed +is_step_completed() { + local step="$1" + [[ " ${COMPLETED_STEPS[*]} " =~ " ${step} " ]] +} + +# Mark step as completed +mark_step_completed() { + local step="$1" + if ! is_step_completed "$step"; then + COMPLETED_STEPS+=("$step") + fi + save_state "$step" "completed" +} + +# Mark step as failed +mark_step_failed() { + local step="$1" + save_state "$step" "failed" +} + +#====================================== +# Command Line Argument Parsing +#====================================== + +show_help() { + cat << EOF +Enhanced Dotfiles Installation Script + +USAGE: + $0 [OPTIONS] + +OPTIONS: + -h, --help Show this help message + -r, --resume Resume from last failed step + -u, --update Update existing dotfiles and packages + -v, --verbose Enable verbose output + -n, --dry-run Show what would be done without executing + -f, --force Force reinstallation of components + --step STEP Run only specific step + --skip STEP Skip specific step + --list-steps List all available steps + --status Show current installation status + --clean Clean up state and backup files + +STEPS: +EOF + + for step in "${STEP_ORDER[@]}"; do + printf " %-20s %s\n" "$step" "${INSTALLATION_STEPS[$step]}" + done + + cat << EOF + +EXAMPLES: + $0 # Full installation + $0 --resume # Resume from last failed step + $0 --update # Update existing installation + $0 --step install_packages # Run only package installation + $0 --skip setup_ssh # Skip SSH setup + $0 --dry-run # Preview what would be done + +EOF +} + +parse_arguments() { + local specific_steps=() + local skip_steps=() + + while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_help + exit 0 + ;; + -r|--resume) + RESUME_MODE=true + shift + ;; + -u|--update) + UPDATE_MODE=true + shift + ;; + -v|--verbose) + VERBOSE_MODE=true + shift + ;; + -n|--dry-run) + DRY_RUN=true + shift + ;; + -f|--force) + FORCE_MODE=true + shift + ;; + --step) + if [[ -n "${2:-}" ]]; then + specific_steps+=("$2") + shift 2 + else + print_error "Option --step requires a step name" + exit 1 + fi + ;; + --skip) + if [[ -n "${2:-}" ]]; then + skip_steps+=("$2") + shift 2 + else + print_error "Option --skip requires a step name" + exit 1 + fi + ;; + --list-steps) + echo "Available installation steps:" + for step in "${STEP_ORDER[@]}"; do + printf " %-20s %s\n" "$step" "${INSTALLATION_STEPS[$step]}" + done + exit 0 + ;; + --status) + show_status + exit 0 + ;; + --clean) + cleanup_files + exit 0 + ;; + *) + print_error "Unknown option: $1" + show_help + exit 1 + ;; + esac + done + + # Apply step filters + if [[ ${#specific_steps[@]} -gt 0 ]]; then + STEP_ORDER=("${specific_steps[@]}") + fi + + if [[ ${#skip_steps[@]} -gt 0 ]]; then + local filtered_steps=() + for step in "${STEP_ORDER[@]}"; do + if [[ ! " ${skip_steps[*]} " =~ " ${step} " ]]; then + filtered_steps+=("$step") + fi + done + STEP_ORDER=("${filtered_steps[@]}") + fi +} + +#====================================== +# Status and Cleanup Functions +#====================================== + +show_status() { + print_header "Installation Status" + + if [[ -f "$STATE_FILE" ]]; then + load_state + + print_section "Current State" + print_info "Last step: ${LAST_STEP:-unknown}" + print_info "Step status: ${STEP_STATUS:-unknown}" + print_info "Timestamp: $(date -d "@${TIMESTAMP:-0}" 2>/dev/null || echo "unknown")" + + print_section "Completed Steps" + if [[ ${#COMPLETED_STEPS[@]} -gt 0 ]]; then + for step in "${COMPLETED_STEPS[@]}"; do + print_success "$step: ${INSTALLATION_STEPS[$step]:-unknown}" + done + else + print_info "No steps completed yet" + fi + + print_section "Remaining Steps" + local remaining_steps=() + for step in "${STEP_ORDER[@]}"; do + if ! is_step_completed "$step"; then + remaining_steps+=("$step") + fi + done + + if [[ ${#remaining_steps[@]} -gt 0 ]]; then + for step in "${remaining_steps[@]}"; do + print_warning "$step: ${INSTALLATION_STEPS[$step]:-unknown}" + done + echo + print_info "Run with --resume to continue from where you left off" + else + print_success "All steps completed!" + fi + else + print_info "No installation state found" + print_info "Run the script to start a new installation" + fi +} + +cleanup_files() { + print_header "Cleanup" + + local files_to_clean=( + "$STATE_FILE" + "$LOG_FILE" + ) + + # Find backup directories + mapfile -t backup_dirs < <(find "$HOME" -maxdepth 1 -name ".dotfiles-backup-*" -type d 2>/dev/null || true) + + if [[ ${#backup_dirs[@]} -gt 0 ]]; then + print_section "Backup Directories Found" + for dir in "${backup_dirs[@]}"; do + print_info "$(basename "$dir") - $(ls -la "$dir" 2>/dev/null | wc -l) files" + done + + if prompt_user "Remove backup directories?"; then + for dir in "${backup_dirs[@]}"; do + rm -rf "$dir" && print_success "Removed $(basename "$dir")" + done + fi + fi + + print_section "State and Log Files" + for file in "${files_to_clean[@]}"; do + if [[ -f "$file" ]]; then + print_info "Found: $file" + if prompt_user "Remove $(basename "$file")?"; then + rm -f "$file" && print_success "Removed $(basename "$file")" + fi + fi + done + + print_success "Cleanup completed" +} -# Move log file to Trash directory -mv -f "$LOG_FILE" "$TRASH_DIR/" +#====================================== +# UI Functions (keeping existing ones and adding new) +#====================================== -# Redirect stderr to both stderr and log file -exec 2> >(tee -a "$LOG_FILE") +# Print colorized output +print_color() { + local color="$1" + local message="$2" + echo -e "${color}${message}${NOCOLOR}" -# Function to log errors -log_error() { + # Log to file if logging is setup + if [[ -n "${LOG_FILE:-}" && -f "$LOG_FILE" ]]; then + echo "$(date +'%Y-%m-%d %H:%M:%S') - $message" >> "$LOG_FILE" + fi +} + +# Print header with decorative border +print_header() { + local title="$1" + local border_char="=" + local border_length=60 + + echo + print_color "$CYAN" "$(printf '%*s' $border_length '' | tr ' ' "$border_char")" + print_color "$CYAN$BOLD" "$(printf '%*s' $(((border_length + ${#title}) / 2)) "$title")" + print_color "$CYAN" "$(printf '%*s' $border_length '' | tr ' ' "$border_char")" + echo +} + +# Print section header +print_section() { + local title="$1" + echo + print_color "$BLUE$BOLD" "▶ $title" + print_color "$BLUE" "$(printf '%*s' $((${#title} + 2)) '' | tr ' ' '-')" +} + +# Print success message +print_success() { local message="$1" - echo "[ERROR] $(date +'%Y-%m-%d %H:%M:%S') - $message" | tee -a "$LOG_FILE" >&2 + print_color "$GREEN" "✓ $message" + INSTALL_SUMMARY+=("✓ $message") } -# Function to handle errors -handle_error() { +# Print error message +print_error() { local message="$1" - log_error "$message" - exit 1 + print_color "$RED" "✗ $message" >&2 + FAILED_ITEMS+=("✗ $message") } -# Function to log completion messages -log_complete() { +# Print warning message +print_warning() { local message="$1" - echo "[COMPLETE] $(date +'%Y-%m-%d %H:%M:%S') - $message" | tee -a "$LOG_FILE" + print_color "$YELLOW" "⚠ $message" } -# Function to handle completion -handle_complete() { +# Print info message +print_info() { local message="$1" - log_complete "$message" + if [[ "$VERBOSE_MODE" == true ]] || [[ "${2:-}" == "always" ]]; then + print_color "$CYAN" "ℹ $message" + fi +} + +# Print skip message +print_skip() { + local message="$1" + print_color "$YELLOW" "⏭ $message" + SKIPPED_ITEMS+=("⏭ $message") +} + +# Print dry run message +print_dry_run() { + local message="$1" + print_color "$MAGENTA" "[DRY RUN] $message" +} + +#====================================== +# Logging Functions (enhanced) +#====================================== + +# Setup logging +setup_logging() { + local log_dir + log_dir="$(dirname "$LOG_FILE")" + + # Create log directory if it doesn't exist + if [[ ! -d "$log_dir" ]]; then + mkdir -p "$log_dir" || { + print_error "Failed to create log directory: $log_dir" + exit 1 + } + fi + + # Create trash directory if it doesn't exist + if [[ ! -d "$TRASH_DIR" ]]; then + mkdir -p "$TRASH_DIR" || { + print_error "Failed to create trash directory: $TRASH_DIR" + exit 1 + } + fi + + # Archive old log file if it exists + if [[ -f "$LOG_FILE" ]]; then + local archived_log="$TRASH_DIR/dotfiles_install_$(date +%Y%m%d_%H%M%S).log" + mv "$LOG_FILE" "$archived_log" + print_info "Archived previous log to: $archived_log" "always" + fi + + # Initialize log file + { + echo "=======================================" + echo "Enhanced Dotfiles Installation Log" + echo "Date: $(date)" + echo "User: $USER" + echo "Host: $HOSTNAME" + echo "OS: $(uname -s)" + echo "Args: $*" + echo "Resume Mode: $RESUME_MODE" + echo "Update Mode: $UPDATE_MODE" + echo "Verbose Mode: $VERBOSE_MODE" + echo "Dry Run: $DRY_RUN" + echo "Force Mode: $FORCE_MODE" + echo "=======================================" + echo + } > "$LOG_FILE" + + print_info "Log file initialized: $LOG_FILE" "always" +} + +# Enhanced log function +log_message() { + local level="$1" + local message="$2" + local timestamp="$(date +'%Y-%m-%d %H:%M:%S')" + echo "[$level] $timestamp - $message" >> "$LOG_FILE" + + if [[ "$VERBOSE_MODE" == true ]]; then + case "$level" in + ERROR) print_color "$RED" "[$level] $message" ;; + WARN) print_color "$YELLOW" "[$level] $message" ;; + INFO) print_color "$CYAN" "[$level] $message" ;; + *) echo "[$level] $message" ;; + esac + fi } -# Function to prompt the user +#====================================== +# User Interaction Functions (enhanced) +#====================================== + +# Enhanced prompt function prompt_user() { - local prompt="$1 [Y/n] " - local default_response="${2:-Y}" + local question="$1" + local default="${2:-Y}" local response + local timeout="${3:-0}" - read -p "$prompt" -n 1 -r -e -i "$default_response" response - case "${response^^}" in - Y) return 0 ;; - N) return 1 ;; - *) handle_error "Invalid choice. Exiting.." && exit ;; - esac -} + # Skip prompts in non-interactive mode or when forcing + if [[ "$FORCE_MODE" == true ]]; then + print_info "Auto-answering '$question' with: $default" + [[ "$default" =~ ^[Yy] ]] && return 0 || return 1 + fi -# Function to temporarily unset GIT_WORK_TREE -function git_without_work_tree() { - # Check if the current directory is a Git repository - if [ -d "$PWD/.git" ]; then - # Check if the current directory is inside the work tree - if [ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" = "true" ]; then - # If it's a Git repository and inside the work tree, proceed with unsetting GIT_WORK_TREE - GIT_WORK_TREE_OLD="$GIT_WORK_TREE" - unset GIT_WORK_TREE - "$@" - export GIT_WORK_TREE="$GIT_WORK_TREE_OLD" + while true; do + if [[ "$default" == "Y" ]]; then + print_color "$YELLOW" "$question [Y/n]: " else - # If it's a Git repository but not inside the work tree, call git command directly - git "$@" + print_color "$YELLOW" "$question [y/N]: " fi - else - # If it's not a Git repository, call git command directly - git "$@" - fi + + if [[ "$timeout" -gt 0 ]]; then + if ! read -t "$timeout" -r response; then + print_info "Timed out, using default: $default" + response="$default" + fi + else + read -r response + fi + + # Use default if no response + if [[ -z "$response" ]]; then + response="$default" + fi + + case "${response^^}" in + Y|YES) return 0 ;; + N|NO) return 1 ;; + *) print_warning "Please answer Y/yes or N/no" ;; + esac + done +} + +# Progress indicator +show_progress() { + local current="$1" + local total="$2" + local message="$3" + local percent=$((current * 100 / total)) + local filled=$((percent / 2)) + local empty=$((50 - filled)) + + printf "\r" + print_color "$BLUE" "[$current/$total] " + printf "%s" "$(printf '█%.0s' $(seq 1 $filled))" + printf "%s" "$(printf '░%.0s' $(seq 1 $empty))" + print_color "$BLUE" " ${percent}%% - $message" } -# Set alias conditionally -alias git='git_without_work_tree' +#====================================== +# System Detection Functions (keeping existing) +#====================================== -# Check for privilege escalation tools -#-------------------------------------- -check_privilege_tools() { - if [ -x "$(command -v sudo)" ]; then +# Detect operating system +detect_os() { + case "$(uname -s)" in + Linux) CFG_OS="linux" ;; + Darwin) CFG_OS="macos" ;; + MINGW*|MSYS*|CYGWIN*) CFG_OS="windows" ;; + *) CFG_OS="unknown" ;; + esac + + print_info "Detected OS: $CFG_OS" "always" + log_message "INFO" "Detected operating system: $CFG_OS" +} + +# Detect privilege escalation tools +detect_privilege_tools() { + if command -v sudo &>/dev/null; then PRIVILEGE_TOOL="sudo" - elif [ -x "$(command -v doas)" ]; then + elif command -v doas &>/dev/null; then PRIVILEGE_TOOL="doas" - elif [ -x "$(command -v pkexec)" ]; then + elif command -v pkexec &>/dev/null; then PRIVILEGE_TOOL="pkexec" - elif [ -x "$(command -v dzdo)" ]; then - PRIVILEGE_TOOL="dzdo" - elif [ "$(id -u)" -eq 0 ]; then - PRIVILEGE_TOOL="" # root + elif [[ "$(id -u)" -eq 0 ]]; then + PRIVILEGE_TOOL="" # Running as root else - PRIVILEGE_TOOL="" # No privilege escalation mechanism found - printf "\n${RED}Error: No privilege escalation tool (sudo, doas, pkexec, dzdo, or root privileges) found. You may not have sufficient permissions to run this script.${NOCOLOR}\n" - printf "\nAttempt to continue Installation (might fail without a privilege escalation tool)? [yes/no] " - read continue_choice - case $continue_choice in - [Yy] | [Yy][Ee][Ss]) ;; - [Nn] | [Nn][Oo]) exit ;; - *) handle_error "Invalid choice. Exiting..." && exit ;; - esac + PRIVILEGE_TOOL="" + print_warning "No privilege escalation tool found" + if prompt_user "Continue without privilege escalation? (Installation may fail for some components)" "N"; then + print_info "Continuing without privilege escalation..." + else + print_error "Privilege escalation required. Exiting." + exit 1 + fi fi + + [[ -n "$PRIVILEGE_TOOL" ]] && print_success "Using privilege escalation tool: $PRIVILEGE_TOOL" } -# Function to set locale to en_US.UTF-8 -set_locale() { - echo "Setting locale to en_US.UTF-8..." - if ! "$PRIVILEGE_TOOL" localectl set-locale LANG=en_US.UTF-8; then - handle_error "Failed to set locale to en_US.UTF-8" +# Detect Linux distribution +detect_linux_distro() { + if [[ ! -f /etc/os-release ]]; then + print_error "/etc/os-release not found" + return 1 fi + + source /etc/os-release + + case "$ID" in + arch|manjaro|endeavouros) DISTRO="PACMAN" ;; + debian|ubuntu|mint|pop) DISTRO="APT" ;; + fedora|rhel|centos|rocky) DISTRO="DNF" ;; + opensuse*|sles) DISTRO="ZYPPER" ;; + gentoo) DISTRO="PORTAGE" ;; + *) + print_warning "Unknown distribution: $ID" + # Try to detect package managers + for pm in pacman apt dnf zypper emerge; do + if command -v "$pm" &>/dev/null; then + case "$pm" in + pacman) DISTRO="PACMAN" ;; + apt) DISTRO="APT" ;; + dnf) DISTRO="DNF" ;; + zypper) DISTRO="ZYPPER" ;; + emerge) DISTRO="PORTAGE" ;; + esac + break + fi + done + + if [[ -z "${DISTRO:-}" ]]; then + print_error "Could not detect package manager" + return 1 + fi + ;; + esac + + print_success "Detected Linux distribution: $ID (Package manager: $DISTRO)" + log_message "INFO" "Detected Linux distribution: $ID, Package manager: $DISTRO" } -# Change shell to zsh -change_shell() { - if prompt_user "Change shell to zsh?"; then - if command -v zsh &>/dev/null; then - chsh -s "$(which zsh)" && echo "Shell changed to zsh. Please log out and log back in to apply the changes." - #sudo chsh -s "$(which zsh)" "$(whoami)" +#====================================== +# Enhanced Utility Functions +#====================================== + +# Check if command exists +command_exists() { + command -v "$1" &>/dev/null +} + +# Execute with dry run support +execute_command() { + local cmd="$*" + log_message "INFO" "Executing: $cmd" + + if [[ "$DRY_RUN" == true ]]; then + print_warning "DRY RUN MODE - No changes will be made" + echo + fi + + print_info "Starting installation for user: $USER" "always" + print_info "Log file: $LOG_FILE" "always" + print_info "Mode: $( + [[ "$RESUME_MODE" == true ]] && echo "Resume" || + [[ "$UPDATE_MODE" == true ]] && echo "Update" || + echo "Fresh Install" + )" "always" + + # Handle resume mode + if [[ "$RESUME_MODE" == true ]]; then + if load_state; then + print_info "Resuming from previous installation..." "always" + print_info "Last step: ${LAST_STEP:-unknown}" "always" + print_info "Step status: ${STEP_STATUS:-unknown}" "always" + + # Load completed steps from state + if [[ -n "${COMPLETED_STEPS:-}" ]]; then + eval "COMPLETED_STEPS=(${COMPLETED_STEPS})" + fi else - echo "Error: zsh is not installed." + print_warning "No previous installation state found" + print_info "Starting fresh installation..." + RESUME_MODE=false fi - else - echo "Shell not changed." fi -} -# Initialize git submodules -submodules() { - echo "Initializing submodule(s)" - git submodule update --init --recursive -} + # Pre-flight checks + detect_os + detect_privilege_tools -# Install Zsh plugins -install_zsh_plugins() { - local zsh_plugins_dir="$HOME/.config/zsh/plugins" + if [[ "$CFG_OS" == "linux" ]]; then + detect_linux_distro || { + print_error "Failed to detect Linux distribution" + exit 1 + } + fi - mkdir -p "$HOME/.config/zsh" - mkdir -p "$zsh_plugins_dir" + # Show installation plan + echo + print_color "$YELLOW$BOLD" "Installation Plan:" + local step_number=1 + for step in "${STEP_ORDER[@]}"; do + local step_desc="${INSTALLATION_STEPS[$step]}" + if is_step_completed "$step" && [[ "$FORCE_MODE" != true ]]; then + print_color "$GREEN" "$step_number. $step_desc (✓ completed)" + else + print_color "$CYAN" "$step_number. $step_desc" + fi + step_number=$((step_number + 1)) + done - if [ ! -d "$zsh_plugins_dir/zsh-you-should-use" ]; then - echo "Installing zsh-you-should-use..." - git clone https://github.com/MichaelAquilina/zsh-you-should-use.git "$zsh_plugins_dir/zsh-you-should-use" - else - echo "zsh-you-should-use is already installed." + echo + if [[ "$FORCE_MODE" != true ]] && ! prompt_user "Continue with installation?"; then + print_info "Installation cancelled by user" + exit 0 fi - if [ ! -d "$zsh_plugins_dir/zsh-syntax-highlighting" ]; then - echo "Installing zsh-syntax-highlighting..." - git clone https://github.com/zsh-users/zsh-syntax-highlighting.git "$zsh_plugins_dir/zsh-syntax-highlighting" + # Execute installation steps + local failed_steps=() + local step_number=1 + local total_steps=${#STEP_ORDER[@]} + + for step in "${STEP_ORDER[@]}"; do + echo + print_color "$MAGENTA$BOLD" "[$step_number/$total_steps] ${INSTALLATION_STEPS[$step]}" + + if execute_step "$step"; then + log_message "INFO" "Step completed successfully: $step" + else + failed_steps+=("$step") + log_message "ERROR" "Step failed: $step" + + # Ask if user wants to continue + if [[ "$FORCE_MODE" != true ]]; then + echo + if ! prompt_user "Step '$step' failed. Continue with remaining steps?" "Y"; then + print_info "Installation stopped by user" + break + fi + fi + fi + + step_number=$((step_number + 1)) + done + + # Post-installation tasks + if [[ ${#failed_steps[@]} -eq 0 ]]; then + print_success "All installation steps completed successfully!" + clear_state else - echo "zsh-syntax-highlighting is already installed." + print_warning "${#failed_steps[@]} steps failed: ${failed_steps[*]}" + save_state "${failed_steps[-1]}" "failed" fi - if [ ! -d "$zsh_plugins_dir/zsh-autosuggestions" ]; then - echo "Installing zsh-autosuggestions..." - git clone https://github.com/zsh-users/zsh-autosuggestions.git "$zsh_plugins_dir/zsh-autosuggestions" - else - echo "zsh-autosuggestions is already installed." + # Show summary + print_installation_summary + + log_message "INFO" "Installation process completed" + + # Exit with appropriate code + [[ ${#failed_steps[@]} -eq 0 ]] && exit 0 || exit 1 +} + +#====================================== +# Error Handling and Cleanup +#====================================== + +# Trap for cleanup on exit +cleanup_on_exit() { + local exit_code=$? + + if [[ $exit_code -ne 0 ]]; then + print_error "Installation interrupted (exit code: $exit_code)" + log_message "ERROR" "Installation interrupted with exit code: $exit_code" + + # Save state for resume + if [[ -n "${current_step:-}" ]]; then + save_state "$current_step" "interrupted" + print_info "State saved. Run with --resume to continue from where you left off" + fi fi + + # Cleanup temporary files if any + if [[ -n "${TEMP_DIR:-}" ]] && [[ -d "$TEMP_DIR" ]]; then + rm -rf "$TEMP_DIR" + fi +} + +# Trap for handling interruptions +handle_interrupt() { + print_warning "Installation interrupted by user" + log_message "WARN" "Installation interrupted by user (SIGINT)" + exit 130 } -#============================================================================== +# Set up traps +trap cleanup_on_exit EXIT +trap handle_interrupt INT #====================================== -# Common Sources/Dependencies +# MacOS and Windows Support Stubs #====================================== -echo "$dotfiles_dir" >>.gitignore -echo "install.sh" >>.gitignore -# Dotfiles -function config { - git --git-dir="$dotfiles_dir"/ --work-tree="$HOME" "$@" +# Install macOS packages (placeholder) +install_macos_packages() { + local packages_file="$1" + + print_info "macOS package installation" + + if ! command_exists brew; then + print_info "Installing Homebrew..." + if execute_command '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'; then + print_success "Homebrew installed" + else + print_error "Failed to install Homebrew" + return 1 + fi + fi + + # Install packages from YAML + local packages=() + if [[ "$DRY_RUN" != true ]]; then + mapfile -t packages < <(yq e '.packages.macos[]' "$packages_file" 2>/dev/null | grep -v "^null$" || true) + fi + + if [[ ${#packages[@]} -gt 0 ]]; then + for package in "${packages[@]}"; do + if execute_command "brew install '$package'"; then + print_success "Installed $package" + else + print_error "Failed to install $package" + fi + done + fi } -# Function to install dotfiles -install_dotfiles() { - # Check if the $dotfiles_dir directory exists - if [ -d "$dotfiles_dir" ]; then - config pull >/dev/null 2>&1 - update=true +# Install Windows packages (placeholder) +install_windows_packages() { + local packages_file="$1" + + print_info "Windows package installation" + print_warning "Windows package installation not fully implemented" + + # Could implement with Chocolatey, Scoop, or winget + if command_exists choco; then + print_info "Using Chocolatey for package management" + # Implementation would go here + elif command_exists scoop; then + print_info "Using Scoop for package management" + # Implementation would go here + elif command_exists winget; then + print_info "Using Windows Package Manager (winget)" + # Implementation would go here else - git clone --bare "$dotfiles_url" "$dotfiles_dir" >/dev/null 2>&1 - update=false + print_warning "No package manager found for Windows" + return 1 fi - std_err_output=$(config checkout 2>&1 >/dev/null) || true +} - if [[ $std_err_output == *"following untracked working tree files would be overwritten"* ]]; then - if [ "$update" = false ]; then - config checkout -- /dev/null 2>&1 +#====================================== +# Additional Utility Functions +#====================================== + +# Check system requirements +check_system_requirements() { + local requirements_met=true + + # Check for required commands + local required_commands=("git" "curl") + for cmd in "${required_commands[@]}"; do + if ! command_exists "$cmd"; then + print_error "Required command not found: $cmd" + requirements_met=false fi + done + + # Check disk space (require at least 1GB free) + local available_space + available_space=$(df "$HOME" | awk 'NR==2 {print $4}') + if [[ "$available_space" -lt 1048576 ]]; then # 1GB in KB + print_warning "Low disk space available: $(($available_space / 1024))MB" fi - config config status.showUntrackedFiles no - git config --global include.path "$HOME.gitconfig.aliases" + # Check internet connectivity + if ! curl -s --head --request GET https://github.com >/dev/null; then + print_warning "No internet connectivity detected" + print_info "Some features may not work properly" + fi - # Prompt the user if they want to overwrite existing files - if prompt_user "Do you want to overwrite existing files and continue with the dotfiles setup?"; then - config fetch origin main:main + return $(($requirements_met ? 0 : 1)) +} - config reset --hard main +# Validate configuration files +validate_config() { + local config_dir="$HOME/.config" + local issues_found=false - config checkout -f - if [ $? -eq 0 ]; then - echo "Successfully backed up conflicting dotfiles in $dotfiles_dir-backup/ and imported $dotfiles_dir." - else - handle_error "Mission failed." + # Check for common configuration issues + if [[ -f "$config_dir/packages.yml" ]]; then + if ! yq e '.' "$config_dir/packages.yml" >/dev/null 2>&1; then + print_error "Invalid YAML syntax in packages.yml" + issues_found=true fi - else - # User chose not to overwrite existing files - handle_error "Aborted by user. Exiting..." fi + + # Check for conflicting dotfiles + local common_conflicts=(".bashrc" ".zshrc" ".vimrc" ".gitconfig") + for file in "${common_conflicts[@]}"; do + if [[ -f "$HOME/$file" ]] && [[ ! -L "$HOME/$file" ]]; then + print_warning "Potential conflict: $HOME/$file exists and is not a symlink" + fi + done + + return $(($issues_found ? 1 : 0)) } -# Check if necessary dependencies are installed -#-------------------------------------- -# Download dependencies (wget/curl) -check_download_dependencies() { - if [ -x "$(command -v wget)" ]; then - DOWNLOAD_COMMAND="wget" - elif [ -x "$(command -v curl)" ]; then - DOWNLOAD_COMMAND="curl" - else - handle_error "Neither wget nor curl found. Please install one of them to continue!" +#====================================== +# Script Entry Point +#====================================== + +# Execute the main function if script is run directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + # Check system requirements first + if ! check_system_requirements; then + print_error "System requirements not met" + exit 1 fi + + # Run main installation + main "$@" +fi + print_dry_run "$cmd" + return 0 + fi + + if [[ "$VERBOSE_MODE" == true ]]; then + print_info "Running: $cmd" + fi + + eval "$cmd" } -# Download a file using wget or curl +# Download file with progress download_file() { local url="$1" local output="$2" - if [ "$DOWNLOAD_COMMAND" = "wget" ]; then - if ! wget -q --show-progress -O "$output" "$url"; then - handle_error "Download failed. Exiting..." - exit 1 - fi - elif [ "$DOWNLOAD_COMMAND" = "curl" ]; then - if ! curl --progress-bar -# -o "$output" "$url"; then - handle_error "Download failed. Exiting..." - exit 1 - fi + if [[ "$DRY_RUN" == true ]]; then + print_dry_run "Download: $url -> $output" + return 0 + fi + + if command_exists wget; then + wget --progress=bar:force -O "$output" "$url" 2>&1 | \ + while IFS= read -r line; do + if [[ "$line" =~ [0-9]+% ]]; then + printf "\r%s" "$line" + fi + done + echo + elif command_exists curl; then + curl --progress-bar -o "$output" "$url" else - echo "Unsupported download command: $DOWNLOAD_COMMAND" - exit 1 + print_error "Neither wget nor curl found" + return 1 fi } -# Install yq -install_yq() { - if ! command -v yq &>/dev/null; then - echo "yq not found, installing..." - local bin_dir="$HOME/.local/bin" - local yq_url="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64" - local yq_path="$bin_dir/yq" +# Create directory with proper permissions +create_dir() { + local dir="$1" + local permissions="${2:-755}" - echo "Installing yq..." + if [[ "$DRY_RUN" == true ]]; then + print_dry_run "Create directory: $dir (mode: $permissions)" + return 0 + fi - # Create bin directory if it doesn't exist - mkdir -p "$bin_dir" || { - echo "Error: Failed to create directory $bin_dir" + if [[ ! -d "$dir" ]]; then + mkdir -p "$dir" || { + print_error "Failed to create directory: $dir" return 1 } + chmod "$permissions" "$dir" + print_success "Created directory: $dir" + else + print_info "Directory already exists: $dir" + fi +} - # Download yq - download_file "$yq_url" "$yq_path" || return 1 +# Backup existing files +backup_file() { + local file="$1" + local backup_path="$BACKUP_DIR/$(dirname "${file#$HOME/}")" - # Make yq executable - chmod +x "$yq_path" || { - echo "Error: Failed to set executable permissions for $yq_path" - return 1 - } + if [[ "$DRY_RUN" == true ]]; then + print_dry_run "Backup: $file -> $backup_path" + return 0 + fi + + if [[ -e "$file" ]]; then + mkdir -p "$backup_path" + cp -a "$file" "$backup_path/" + print_info "Backed up: $file" + return 0 + fi + return 1 +} - echo "yq installed successfully to $bin_dir." +#====================================== +# Git Configuration Functions (keeping existing) +#====================================== - # Add bin directory to PATH if not already added - if [[ ":$PATH:" != *":$bin_dir:"* ]]; then - echo "Adding $bin_dir to PATH" - echo "export PATH=\"$bin_dir:\$PATH\"" >>"$HOME/.bashrc" - export PATH="$bin_dir:$PATH" - fi +# Git wrapper to avoid conflicts +git_without_work_tree() { + if [[ -d "$PWD/.git" ]] && [[ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" == "true" ]]; then + local old_work_tree="$GIT_WORK_TREE" + unset GIT_WORK_TREE + git "$@" + export GIT_WORK_TREE="$old_work_tree" else - echo "yq is already installed." + git "$@" fi } -#------------------------------------------------------------------------------ +# Dotfiles Management System (keeping existing config function) +if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then + # Core git wrapper with repository as work-tree + _config() { + git --git-dir="$DOTFILES_DIR" --work-tree="$HOME" "$@" + } + + # Map system path to repository path + _repo_path() { + local f="$1" + local relative_path="${f#$HOME/}" + local repo_path -#============================================================================== + # If it's an absolute path that's not in HOME, handle it specially + if [[ "$f" == /* && "$f" != "$HOME/"* ]]; then + echo "$CFG_OS/root/$f" + return + fi + + # Check for paths that are explicitly within the repo structure + case "$f" in + "$HOME/.cfg/"*) + echo "" + return + ;; + "common/"*) + echo "$f" + return + ;; + "$CFG_OS/"*) + echo "$f" + return + ;; + *) + echo "$CFG_OS/home/$relative_path" + return + ;; + esac + } + + # Map repository path back to system path + _sys_path() { + local repo_path="$1" + local file_path + + case "$repo_path" in + common/config/*) + file_path="${repo_path#common/config/}" + if [[ "$CFG_OS" == "windows" ]]; then + echo "$HOME/AppData/Local/$file_path" + else + echo "$HOME/.config/$file_path" + fi + ;; + common/bin/*) + file_path="${repo_path#common/bin/}" + if [[ "$CFG_OS" == "windows" ]]; then + echo "$HOME/bin/$file_path" + else + echo "$HOME/.local/bin/$file_path" + fi + ;; + common/*) + file_path="${repo_path#common/}" + echo "$HOME/$file_path" + ;; + */home/*) + file_path="${repo_path#*/home/}" + echo "$HOME/$file_path" + ;; + */root/*) + file_path="${repo_path#*/root/}" + echo "/$file_path" + ;; + *) + echo "$HOME/$repo_path" + ;; + esac + } + + # Prompts for sudo if needed and runs the command + _sudo_prompt() { + if [[ $EUID -eq 0 ]]; then + "$@" + else + if command -v sudo >/dev/null; then + sudo "$@" + elif command -v doas >/dev/null; then + doas "$@" + elif command -v pkexec >/dev/null; then + pkexec "$@" + else + echo "Error: No privilege escalation tool (sudo, doas, pkexec) found." + return 1 + fi + fi + } + + # Enhanced config command + config() { + local cmd="$1"; shift + case "$cmd" in + add) + local file_path + for file_path in "$@"; do + local repo_path="$(_repo_path "$file_path")" + if [[ -z "$repo_path" ]]; then + echo "Warning: Ignoring file within the bare repo: $file_path" + continue + fi + local full_repo_path="$HOME/.cfg/$repo_path" + mkdir -p "$(dirname "$full_repo_path")" + cp -a "$file_path" "$full_repo_path" + _config add "$repo_path" + echo "Added: $file_path -> $repo_path" + done + ;; + rm) + local rm_opts="" + local file_path_list=() + + # Separate options from file paths + for arg in "$@"; do + if [[ "$arg" == "-"* ]]; then + rm_opts+=" $arg" + else + file_path_list+=("$arg") + fi + done + + for file_path in "${file_path_list[@]}"; do + local repo_path="$(_repo_path "$file_path")" + + # Use a dummy run of `git rm` to handle the recursive flag + if [[ "$rm_opts" == *"-r"* ]]; then + _config rm --cached -r "$repo_path" + else + _config rm --cached "$repo_path" + fi + + # Remove from the filesystem, passing the collected options + eval "rm $rm_opts \"$file_path\"" + + echo "Removed: $file_path" + done + ;; + sync) + local direction="${1:-to-repo}"; shift + _config ls-files | while read -r repo_file; do + local sys_file="$(_sys_path "$repo_file")" + local full_repo_path="$HOME/.cfg/$repo_file" + if [[ "$direction" == "to-repo" ]]; then + if [[ -e "$sys_file" && -n "$(diff "$full_repo_path" "$sys_file" 2>/dev/null || true)" ]]; then + cp -a "$sys_file" "$full_repo_path" + echo "Synced to repo: $sys_file" + fi + elif [[ "$direction" == "from-repo" ]]; then + if [[ -e "$full_repo_path" && -n "$(diff "$full_repo_path" "$sys_file" 2>/dev/null || true)" ]]; then + local dest_dir="$(dirname "$sys_file")" + if [[ "$sys_file" == "/etc"* || "$sys_file" == "/usr"* ]]; then + _sudo_prompt cp -a "$full_repo_path" "$sys_file" + else + mkdir -p "$dest_dir" + cp -a "$full_repo_path" "$sys_file" + fi + echo "Synced from repo: $sys_file" + fi + fi + done + ;; + status) + local auto_synced=() + while read -r repo_file; do + local sys_file="$(_sys_path "$repo_file")" + local full_repo_path="$HOME/.cfg/$repo_file" + if [[ -e "$sys_file" && -e "$full_repo_path" ]]; then + if ! diff -q "$full_repo_path" "$sys_file" >/dev/null 2>&1; then + \cp -fa "$sys_file" "$full_repo_path" + auto_synced+=("$repo_file") + fi + fi + done < <(_config ls-files) + if [[ ${#auto_synced[@]} -gt 0 ]]; then + echo "=== Auto-synced Files ===" + for repo_file in "${auto_synced[@]}"; do + echo "synced: $(_sys_path "$repo_file") → $repo_file" + done + echo + fi + _config status + echo + ;; + deploy) + _config ls-files | while read -r repo_file; do + local sys_file="$(_sys_path "$repo_file")" + local full_repo_path="$HOME/.cfg/$repo_file" + if [[ -e "$full_repo_path" ]]; then + if [[ -n "$sys_file" ]]; then + local dest_dir="$(dirname "$sys_file")" + if [[ "$sys_file" == "/etc"* || "$sys_file" == "/usr"* ]]; then + _sudo_prompt mkdir -p "$dest_dir" + _sudo_prompt cp -a "$full_repo_path" "$sys_file" + else + mkdir -p "$dest_dir" + cp -a "$full_repo_path" "$sys_file" + fi + echo "Deployed: $repo_file -> $sys_file" + fi + fi + done + ;; + backup) + local timestamp=$(date +%Y%m%d%H%M%S) + local backup_dir="$HOME/.dotfiles_backup/$timestamp" + echo "Backing up existing dotfiles to $backup_dir..." + + _config ls-files | while read -r repo_file; do + local sys_file="$(_sys_path "$repo_file")" + if [[ -e "$sys_file" ]]; then + local dest_dir_full="$backup_dir/$(dirname "$repo_file")" + mkdir -p "$dest_dir_full" + cp -a "$sys_file" "$backup_dir/$repo_file" + fi + done + echo "Backup complete. To restore, copy files from $backup_dir to their original locations." + ;; + *) + _config "$cmd" "$@" + ;; + esac + } +fi #====================================== -# Check Operating System +# Enhanced Installation Functions #====================================== -check_os() { - if [[ "$OSTYPE" == "linux-gnu"* ]]; then - echo "Linux OS detected." - # Implement Linux-specific checks - elif [[ "$OSTYPE" == "darwin"* ]]; then - echo "MacOS detected." - # Implement MacOS-specific checks - elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then - echo "Windows-like environment detected." - # Implement Windows-specific checks + +# Install dotfiles +install_dotfiles() { + print_section "Installing Dotfiles" + save_state "install_dotfiles" "started" + + local update=false + + if [[ -d "$DOTFILES_DIR" ]]; then + if [[ "$UPDATE_MODE" == true ]] || prompt_user "Dotfiles repository already exists. Update it?"; then + print_info "Updating existing dotfiles..." + if execute_command "config pull origin main"; then + update=true + print_success "Dotfiles updated successfully" + else + print_error "Failed to pull updates" + mark_step_failed "install_dotfiles" + return 1 + fi + else + print_skip "Skipping dotfiles update" + mark_step_completed "install_dotfiles" + return 0 + fi else - handle_error "Unsupported operating system." + print_info "Cloning dotfiles repository..." + if execute_command "git clone --bare '$DOTFILES_URL' '$DOTFILES_DIR'"; then + print_success "Dotfiles repository cloned" + else + print_error "Failed to clone dotfiles repository" + mark_step_failed "install_dotfiles" + return 1 + fi + fi + + # Check for conflicts only if not updating + if [[ "$update" != true ]]; then + local conflicts + conflicts=$(config checkout 2>&1 | grep -E "^\s+" | awk '{print $1}' || true) + + if [[ -n "$conflicts" ]]; then + print_warning "The following files will be overwritten:" + echo "$conflicts" + + if [[ "$FORCE_MODE" == true ]] || prompt_user "Continue and backup/overwrite these files?"; then + # Backup conflicting files + create_dir "$BACKUP_DIR" + print_info "Backing up conflicting files to: $BACKUP_DIR" + + while IFS= read -r file; do + [[ -z "$file" ]] && continue + backup_file "$HOME/$file" + done <<< "$conflicts" + + print_info "Backed up conflicting files to: $BACKUP_DIR" + else + print_error "Installation cancelled by user" + mark_step_failed "install_dotfiles" + return 1 + fi + fi + + # Checkout files + if execute_command "config checkout -f"; then + print_success "Dotfiles checked out successfully" + else + print_error "Failed to checkout dotfiles" + mark_step_failed "install_dotfiles" + return 1 + fi fi + + # Configure repository + execute_command "config config status.showUntrackedFiles no" + + mark_step_completed "install_dotfiles" + print_success "Dotfiles installed successfully" } -#============================================================================== +# Create user directories +setup_user_dirs() { + print_section "Setting Up User Directories" + save_state "setup_user_dirs" "started" -#====================================== -# Linux -#====================================== + local directories=('.cache' '.config' '.local/bin' '.local/share' '.scripts') + + for dir in "${directories[@]}"; do + create_dir "$HOME/$dir" + done + + # Handle XDG user directories + if [[ -f "$HOME/.config/user-dirs.dirs" ]]; then + if [[ "$FORCE_MODE" == true ]] || prompt_user "Configure XDG user directories?"; then + if [[ "$DRY_RUN" != true ]]; then + source "$HOME/.config/user-dirs.dirs" + fi -# Check Distro -#-------------------------------------- + # Create XDG directories + for var in XDG_DESKTOP_DIR XDG_DOWNLOAD_DIR XDG_TEMPLATES_DIR XDG_PUBLICSHARE_DIR \ + XDG_DOCUMENTS_DIR XDG_MUSIC_DIR XDG_PICTURES_DIR XDG_VIDEOS_DIR; do + local dir_path="${!var:-}" + [[ -n "$dir_path" ]] && create_dir "$dir_path" + done -# Detect package type from /etc/issue -_found_arch() { - local _ostype="$1" - shift - grep -qis "$*" /etc/issue && _distro="$_ostype" + print_success "XDG user directories configured" + fi + fi + + mark_step_completed "setup_user_dirs" } -# Detect package type -_distro_detect() { - # Check if /etc/os-release exists and extract information - if [ -f /etc/os-release ]; then - source /etc/os-release - case "$ID" in - "arch") - _distro="PACMAN" - return +# Enhanced yq installation +install_yq() { + local bin_dir="$HOME/.local/bin" + local yq_path="$bin_dir/yq" + + if command_exists yq && [[ "$FORCE_MODE" != true ]]; then + print_info "yq already available" + return 0 + fi + + local yq_url="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64" + + case "$CFG_OS" in + linux) + case "$(uname -m)" in + x86_64) yq_url="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64" ;; + aarch64) yq_url="https://github.com/mikefarah/yq/releases/latest/download/yq_linux_arm64" ;; + *) print_error "Unsupported architecture for yq installation"; return 1 ;; + esac ;; - "debian") - _distro="DPKG" - return + macos) + yq_url="https://github.com/mikefarah/yq/releases/latest/download/yq_darwin_amd64" ;; - "ubuntu") - _distro="DPKG" - return + *) + print_error "yq installation not supported for $CFG_OS" + return 1 ;; - "centos") - _distro="YUM" - return + esac + + print_info "Installing yq..." + + create_dir "$bin_dir" + download_file "$yq_url" "$yq_path" || return 1 + execute_command "chmod +x '$yq_path'" || return 1 + + # Add to PATH if not already there + if [[ ":$PATH:" != *":$bin_dir:"* ]]; then + export PATH="$bin_dir:$PATH" + if [[ "$DRY_RUN" != true ]]; then + echo "export PATH=\"$bin_dir:\$PATH\"" >> "$HOME/.bashrc" + fi + fi + + print_success "yq installed successfully" +} + +# Enhanced package installation +install_packages() { + print_section "Installing Packages" + save_state "install_packages" "started" + + local packages_file="$HOME/.config/packages.yml" + + # Check if yq is available for YAML parsing + if ! command_exists yq; then + if [[ "$FORCE_MODE" == true ]] || prompt_user "yq (YAML parser) is required. Install it?"; then + install_yq || { + print_error "Failed to install yq" + mark_step_failed "install_packages" + return 1 + } + else + print_skip "Package installation (requires yq)" + mark_step_completed "install_packages" + return 0 + fi + fi + + if [[ ! -f "$packages_file" ]]; then + print_warning "packages.yml not found at $packages_file, checking current directory..." + if [[ -f "packages.yml" ]]; then + packages_file="packages.yml" + else + print_warning "packages.yml not found, skipping package installation" + mark_step_completed "install_packages" + return 0 + fi + fi + + case "$CFG_OS" in + linux) + install_linux_packages "$packages_file" ;; - "fedora") - _distro="YUM" - return + macos) + install_macos_packages "$packages_file" ;; - "opensuse" | "suse") - _distro="ZYPPER" - return + windows) + install_windows_packages "$packages_file" ;; - "gentoo") - _distro="PORTAGE" - return + *) + print_warning "Package installation not supported for $CFG_OS" ;; + esac + + mark_step_completed "install_packages" +} + +# Enhanced Linux package installation +install_linux_packages() { + local packages_file="$1" + local failed_packages=() + local installed_packages=() + local skipped_packages=() + + # Get package lists + local base_packages=() + local distro_packages=() + + if [[ "$DRY_RUN" != true ]]; then + mapfile -t base_packages < <(yq e '.packages.base[]' "$packages_file" 2>/dev/null | grep -v "^null$" || true) + + case "$DISTRO" in + PACMAN) + mapfile -t distro_packages < <(yq e '.packages.arch[]' "$packages_file" 2>/dev/null | grep -v "^null$" || true) + ;; + APT) + mapfile -t distro_packages < <(yq e '.packages.debian[]' "$packages_file" 2>/dev/null | grep -v "^null$" || true) + ;; + DNF) + mapfile -t distro_packages < <(yq e '.packages.fedora[]' "$packages_file" 2>/dev/null | grep -v "^null$" || true) + ;; esac fi - # Fallback method if /etc/os-release doesn't provide the information - if [ -f /etc/issue ]; then - _found_arch PACMAN "Arch Linux" && return - _found_arch DPKG "Debian GNU/Linux" && return - _found_arch DPKG "Ubuntu" && return - _found_arch YUM "CentOS" && return - _found_arch YUM "Red Hat" && return - _found_arch YUM "Fedora" && return - _found_arch ZYPPER "SUSE" && return - _found_arch PORTAGE "Gentoo" && return + # Combine package lists + local all_packages=("${base_packages[@]}" "${distro_packages[@]}") + + if [[ ${#all_packages[@]} -eq 0 ]]; then + print_warning "No packages found in configuration" + return 0 fi - # Check for package managers and prompt the user if none found - local available_package_managers=("apt" "pacman" "portage" "yum" "zypper") - for manager in "${available_package_managers[@]}"; do - if command -v "$manager" &>/dev/null; then - _distro="$manager" - return + print_info "Found ${#all_packages[@]} packages to install" + + # Update package database first + if [[ "$UPDATE_MODE" == true ]] || [[ "$FORCE_MODE" == true ]] || prompt_user "Update package database before installing?" "Y" 30; then + print_info "Updating package database..." + case "$DISTRO" in + PACMAN) execute_command "$PRIVILEGE_TOOL pacman -Sy" ;; + APT) execute_command "$PRIVILEGE_TOOL apt update" ;; + DNF) execute_command "$PRIVILEGE_TOOL dnf check-update || true" ;; + ZYPPER) execute_command "$PRIVILEGE_TOOL zypper refresh" ;; + PORTAGE) execute_command "$PRIVILEGE_TOOL emerge --sync" ;; + esac + fi + + # Install packages with progress indicator + local current=0 + for package in "${all_packages[@]}"; do + [[ -z "$package" ]] && continue + + current=$((current + 1)) + show_progress "$current" "${#all_packages[@]}" "$package" + + # Check if package is already installed + local already_installed=false + case "$DISTRO" in + PACMAN) + if pacman -Q "$package" &>/dev/null; then + already_installed=true + fi + ;; + APT) + if dpkg -l "$package" 2>/dev/null | grep -q "^ii"; then + already_installed=true + fi + ;; + DNF) + if rpm -q "$package" &>/dev/null; then + already_installed=true + fi + ;; + esac + + if [[ "$already_installed" == true ]] && [[ "$FORCE_MODE" != true ]]; then + skipped_packages+=("$package") + continue + fi + + # Install package + local install_cmd="" + case "$DISTRO" in + PACMAN) + install_cmd="$PRIVILEGE_TOOL pacman -S --noconfirm '$package'" + ;; + APT) + install_cmd="$PRIVILEGE_TOOL apt install -y '$package'" + ;; + DNF) + install_cmd="$PRIVILEGE_TOOL dnf install -y '$package'" + ;; + esac + + if execute_command "$install_cmd"; then + installed_packages+=("$package") + else + failed_packages+=("$package") fi done - # If none of the above methods work, prompt the user to specify the package manager - printf "Unable to detect the package manager. Please specify the package manager (e.g., apt, pacman, portage, yum, zypper): " - read -r user_package_manager - if [ -x "$(command -v "$user_package_manager")" ]; then - _distro="$user_package_manager" - return - else - _error "Specified package manager '$user_package_manager' not found. Exiting..." - exit 1 + echo # Clear progress line + + # Report results + if [[ ${#installed_packages[@]} -gt 0 ]]; then + print_success "Successfully installed ${#installed_packages[@]} packages" fi + + if [[ ${#skipped_packages[@]} -gt 0 ]]; then + print_info "Skipped ${#skipped_packages[@]} already installed packages" + fi + + if [[ ${#failed_packages[@]} -gt 0 ]]; then + print_error "Failed to install ${#failed_packages[@]} packages: ${failed_packages[*]}" + return 1 + fi + + return 0 } -#------------------------------------------------------------------------------ +# Enhanced shell setup +setup_shell() { + print_section "Setting Up Shell Environment" + save_state "setup_shell" "started" + + # Install Zsh if requested + if [[ "$FORCE_MODE" == true ]] || prompt_user "Install and configure Zsh?"; then + if ! command_exists zsh; then + print_info "Installing Zsh..." + case "$DISTRO" in + PACMAN) execute_command "$PRIVILEGE_TOOL pacman -S --noconfirm zsh zsh-completions" ;; + APT) execute_command "$PRIVILEGE_TOOL apt install -y zsh zsh-autosuggestions zsh-syntax-highlighting" ;; + DNF) execute_command "$PRIVILEGE_TOOL dnf install -y zsh zsh-autosuggestions zsh-syntax-highlighting" ;; + esac + fi -# Define directories to create -user_dirs() { - directories=('.cache' '.config' '.scripts') + if command_exists zsh || [[ "$DRY_RUN" == true ]]; then + if [[ "$FORCE_MODE" == true ]] || prompt_user "Change default shell to Zsh?"; then + local zsh_path + zsh_path="$(which zsh 2>/dev/null || echo "/usr/bin/zsh")" + if execute_command "chsh -s '$zsh_path'"; then + print_success "Default shell changed to Zsh" + print_warning "Please log out and log back in to apply changes" + else + print_error "Failed to change default shell" + fi + fi - # Prompt the user if they want to use user-dirs.dirs - if prompt_user "Do you want to use the directories specified in user-dirs.dirs?"; then - # Check if ~/.config/user-dirs.dirs exists - config_dirs_file="$HOME/.config/user-dirs.dirs" - if [ -f "$config_dirs_file" ]; then - echo "Config file $config_dirs_file exists. Proceeding..." + # Install Zsh plugins + install_zsh_plugins else - echo "Error: Config file $config_dirs_file not found. Please check your configuration." - exit 1 + print_error "Zsh installation failed" + mark_step_failed "setup_shell" + return 1 fi + else + print_skip "Zsh setup" + fi - # Prompt the user if they want to change directory names - if prompt_user "Do you want to change the directory names to lowercase?"; then - # Function to change directory names from uppercase to lowercase - change_dir_names() { - local config_file="$HOME/.config/user-dirs.dirs" - - # Check if the system is not macOS - if [[ ! "$OSTYPE" == "darwin"* ]]; then - # Check if the config file exists - if [ -f "$config_file" ]; then - echo "Changing directory names from uppercase to lowercase..." - - # Read the lines from the config file and process them - while read -r line; do - # Extract variable name and path from each line - if [[ $line =~ ^[[:space:]]*([A-Z_]+)=\"(.+)\" ]]; then - var_name="${BASH_REMATCH[1]}" - var_path="${BASH_REMATCH[2]}" - - # Convert the variable name to lowercase - var_name_lowercase="$(echo "$var_name" | tr '[:upper:]' '[:lower:]')" - - # Check if the directory exists - if [ -d "$var_path" ]; then - # Rename the directory to lowercase - new_var_path="$HOME/${var_name_lowercase}" - mv "$var_path" "$new_var_path" - echo "Renamed $var_path to $new_var_path" - fi - fi - done <"$config_file" + mark_step_completed "setup_shell" +} - echo "Directory names changed successfully." - else - echo "The config file $config_file does not exist. Skipping directory name changes." - fi +# Enhanced Zsh plugin installation +install_zsh_plugins() { + local plugins_dir="$HOME/.config/zsh/plugins" + local plugins=( + "zsh-you-should-use:https://github.com/MichaelAquilina/zsh-you-should-use.git" + "zsh-syntax-highlighting:https://github.com/zsh-users/zsh-syntax-highlighting.git" + "zsh-autosuggestions:https://github.com/zsh-users/zsh-autosuggestions.git" + "powerlevel10k:https://github.com/romkatv/powerlevel10k.git" + ) + + create_dir "$plugins_dir" + + local current=0 + for plugin_info in "${plugins[@]}"; do + local plugin_name="${plugin_info%%:*}" + local plugin_url="${plugin_info##*:}" + local plugin_path="$plugins_dir/$plugin_name" + + current=$((current + 1)) + show_progress "$current" "${#plugins[@]}" "$plugin_name" + + if [[ -d "$plugin_path" ]]; then + if [[ "$UPDATE_MODE" == true ]] || [[ "$FORCE_MODE" == true ]] || prompt_user "Update $plugin_name?" "Y" 10; then + if execute_command "(cd '$plugin_path' && git pull)"; then + print_success "Updated $plugin_name" else - echo "macOS detected. Skipping directory name changes." + print_error "Failed to update $plugin_name" fi - } + else + print_skip "Update for $plugin_name" + fi + else + print_info "Installing $plugin_name..." + if execute_command "git clone --depth=1 '$plugin_url' '$plugin_path'"; then + print_success "Installed $plugin_name" + else + print_error "Failed to install $plugin_name" + fi + fi + done + echo # Clear progress line +} - # Run the function to change directory names - change_dir_names - elif prompt_user "Do you want to change the directory names to uppercase?"; then - # Function to change directory names from lowercase to uppercase - change_dir_names() { - local config_file="$HOME/.config/user-dirs.dirs" - - # Check if the system is not macOS - if [[ ! "$OSTYPE" == "darwin"* ]]; then - # Check if the config file exists - if [ -f "$config_file" ]; then - echo "Changing directory names from lowercase to uppercase..." - - # Read the lines from the config file and process them - while read -r line; do - # Extract variable name and path from each line - if [[ $line =~ ^[[:space:]]*([A-Z_]+)=\"(.+)\" ]]; then - var_name="${BASH_REMATCH[1]}" - var_path="${BASH_REMATCH[2]}" - - # Convert the variable name to uppercase - var_name_uppercase="$(echo "$var_name" | tr '[:lower:]' '[:upper:]')" - - # Check if the directory exists - if [ -d "$var_path" ]; then - # Rename the directory to uppercase - new_var_path="$HOME/${var_name_uppercase}" - mv "$var_path" "$new_var_path" - echo "Renamed $var_path to $new_var_path" - fi - fi - done <"$config_file" +# Setup SSH +setup_ssh() { + print_section "Setting Up SSH" + save_state "setup_ssh" "started" - echo "Directory names changed successfully." - else - echo "The config file $config_file does not exist. Skipping directory name changes." - fi - else - echo "macOS detected. Skipping directory name changes." - fi - } + local ssh_dir="$HOME/.ssh" + + if [[ ! -f "$ssh_dir/id_rsa" && ! -f "$ssh_dir/id_ed25519" ]]; then + if [[ "$FORCE_MODE" == true ]] || prompt_user "Generate SSH key pair?"; then + create_dir "$ssh_dir" 700 - # Run the function to change directory names - change_dir_names - #xdg-user-dirs-update + local email + if [[ "$FORCE_MODE" != true ]]; then + print_color "$YELLOW" "Enter email for SSH key (or press Enter for $USER@$HOSTNAME): " + read -r email + fi + email="${email:-$USER@$HOSTNAME}" + + # Use Ed25519 for better security + local key_type="ed25519" + local key_file="$ssh_dir/id_ed25519" + + if execute_command "ssh-keygen -t '$key_type' -f '$key_file' -N '' -C '$email'"; then + print_success "SSH key pair generated (Ed25519)" + execute_command "cat '$key_file.pub' >> '$ssh_dir/authorized_keys'" + execute_command "chmod 600 '$ssh_dir/authorized_keys'" + print_info "Public key added to authorized_keys" + + # Display public key + if [[ "$DRY_RUN" != true ]] && [[ -f "$key_file.pub" ]]; then + print_info "Your public key:" + print_color "$GREEN" "$(cat "$key_file.pub")" + print_info "Copy this key to your Git hosting service" + fi + else + print_error "Failed to generate SSH key" + mark_step_failed "setup_ssh" + return 1 + fi fi + else + print_info "SSH key already exists" fi - # Create needed dirs and set proper permissions - for d in "${directories[@]}"; do - full_path="$HOME/$d" - if [ ! -d "$full_path" ]; then - mkdir -p "$full_path" - # Assuming $USER is defined or replace it with the desired user - chown -R "$USER" "$full_path" - echo "Created $full_path" - fi - done + mark_step_completed "setup_ssh" } -#------------------------------------------------------------------------------ +# Configure system services +configure_services() { + print_section "Configuring System Services" + save_state "configure_services" "started" -# Update system -linux_update_system() { - # Prompt the user if they want to update the system - prompt_user "Do you want to update the system?" Y || { - echo "System update skipped." - return - } + if [[ "$CFG_OS" != "linux" ]]; then + print_skip "Service configuration (not supported on $CFG_OS)" + mark_step_completed "configure_services" + return 0 + fi - # Continue with system update based on detected package manager - case "$_distro" in - "PACMAN") - "$PRIVILEGE_TOOL" pacman -Syyy && "$PRIVILEGE_TOOL" pacman -Syu --noconfirm - ;; - "DPKG") - "$PRIVILEGE_TOOL" apt-get update && "$PRIVILEGE_TOOL" apt-get upgrade -y - ;; - "YUM") - "$PRIVILEGE_TOOL" yum update -y - ;; - "ZYPPER") - "$PRIVILEGE_TOOL" zypper --non-interactive update - ;; - "PORTAGE") - "$PRIVILEGE_TOOL" emerge --sync && "$PRIVILEGE_TOOL" emerge --ask --update --deep --newuse @world - ;; - *) - echo "Package manager not supported." - return 1 - ;; - esac -} + # Enable TLP for laptop power management + if command_exists tlp; then + print_info "TLP is installed" + if [[ "$FORCE_MODE" == true ]] || prompt_user "Enable TLP power management service?"; then + if execute_command "$PRIVILEGE_TOOL systemctl enable tlp"; then + execute_command "$PRIVILEGE_TOOL systemctl start tlp" + print_success "TLP enabled and started" + else + print_error "Failed to enable TLP" + fi + fi + elif [[ "$FORCE_MODE" == true ]] || prompt_user "Install and enable TLP for better battery life?"; then + case "$DISTRO" in + PACMAN) execute_command "$PRIVILEGE_TOOL pacman -S --noconfirm tlp tlp-rdw" ;; + APT) execute_command "$PRIVILEGE_TOOL apt install -y tlp tlp-rdw" ;; + DNF) execute_command "$PRIVILEGE_TOOL dnf install -y tlp tlp-rdw" ;; + esac -#------------------------------------------------------------------------------ + if command_exists tlp; then + execute_command "$PRIVILEGE_TOOL systemctl enable tlp" + execute_command "$PRIVILEGE_TOOL systemctl start tlp" + print_success "TLP installed, enabled and started" + fi + fi -linux_install_packages() { - local failed_packages=() - local any_failures=false - local packages_file="packages.yml" + # Configure other useful services + local services_to_enable=() - # Check if yq is available - if ! command -v yq &>/dev/null; then - echo "Error: yq (YAML parser) is not installed or not found." - return 1 + # Check for and configure common services + if command_exists docker && ! systemctl is-enabled docker &>/dev/null; then + if [[ "$FORCE_MODE" == true ]] || prompt_user "Enable Docker service?"; then + services_to_enable+=("docker") + fi fi - # Read package names from packages.yml under PackageManager for most distributions - local packages=() - if [[ -f "$packages_file" ]]; then - while IFS= read -r package; do - packages+=("$package") - done < <(yq e '.PackageManager[]' "$packages_file" 2>/dev/null) - else - echo "Error: packages.yml not found." - return 1 + if command_exists bluetooth && ! systemctl is-enabled bluetooth &>/dev/null; then + if [[ "$FORCE_MODE" == true ]] || prompt_user "Enable Bluetooth service?"; then + services_to_enable+=("bluetooth") + fi fi - # Read the package manager type detected by _distro_detect() - case "$_distro" in - "PACMAN") - # Read additional package names from arch section if present - if [[ -f "$packages_file" ]]; then - while IFS= read -r package; do - packages+=("$package") - done < <(yq e '.arch[]' "$packages_file" 2>/dev/null) + # Enable selected services + for service in "${services_to_enable[@]}"; do + if execute_command "$PRIVILEGE_TOOL systemctl enable '$service'"; then + execute_command "$PRIVILEGE_TOOL systemctl start '$service'" + print_success "Enabled and started $service" + else + print_error "Failed to enable $service" fi + done - # Installation using Pacman - for package in "${packages[@]}"; do - if ! pacman -Q "$package" &>/dev/null; then - if ! "$PRIVILEGE_TOOL" pacman -S --noconfirm "$package"; then - failed_packages+=("$package") - any_failures=true - fi - fi - done - ;; - "DPKG") - # Try installing packages with dpkg - for package in "${packages[@]}"; do - if ! dpkg-query -W "$package" &>/dev/null; then - if ! "$PRIVILEGE_TOOL" apt-get install -y "$package"; then - failed_packages+=("$package") - any_failures=true - fi - fi - done - ;; - "YUM") - # Try installing packages with yum - for package in "${packages[@]}"; do - if ! rpm -q "$package" &>/dev/null; then - if ! "$PRIVILEGE_TOOL" yum install -y "$package"; then - failed_packages+=("$package") - any_failures=true - fi - fi - done - ;; - "ZYPPER") - # Try installing packages with zypper - for package in "${packages[@]}"; do - if ! rpm -q "$package" &>/dev/null; then - if ! "$PRIVILEGE_TOOL" zypper --non-interactive install "$package"; then - failed_packages+=("$package") - any_failures=true - fi + mark_step_completed "configure_services" +} + +# Setup development environment +setup_development() { + print_section "Setting Up Development Environment" + save_state "setup_development" "started" + + # Install development tools + local dev_tools=() + + case "$DISTRO" in + PACMAN) dev_tools=("base-devel" "git" "vim" "neovim" "code") ;; + APT) dev_tools=("build-essential" "git" "vim" "neovim" "curl" "wget") ;; + DNF) dev_tools=("@development-tools" "git" "vim" "neovim" "curl" "wget") ;; + esac + + if [[ ${#dev_tools[@]} -gt 0 ]]; then + if [[ "$FORCE_MODE" == true ]] || prompt_user "Install development tools?"; then + local failed_dev_tools=() + for tool in "${dev_tools[@]}"; do + case "$DISTRO" in + PACMAN) + if ! execute_command "$PRIVILEGE_TOOL pacman -S --noconfirm '$tool'"; then + failed_dev_tools+=("$tool") + fi + ;; + APT) + if ! execute_command "$PRIVILEGE_TOOL apt install -y '$tool'"; then + failed_dev_tools+=("$tool") + fi + ;; + DNF) + if ! execute_command "$PRIVILEGE_TOOL dnf install -y '$tool'"; then + failed_dev_tools+=("$tool") + fi + ;; + esac + done + + if [[ ${#failed_dev_tools[@]} -eq 0 ]]; then + print_success "Development tools installed" + else + print_warning "Some development tools failed to install: ${failed_dev_tools[*]}" fi - done - ;; - "PORTAGE") - # Try installing packages with emerge for Gentoo - if [[ -f "$packages_file" ]]; then - # Read package names from packages.yml under gentoo - gentoo_packages=() - while IFS= read -r package; do - gentoo_packages+=("$package") - done < <(yq e '.gentoo[]' "$packages_file" 2>/dev/null) - else - echo "Error: packages.yml not found." - return 1 fi + fi - for package in "${gentoo_packages[@]}"; do - if [ "$package" != "" ]; then - if ! equery list "$package" &>/dev/null; then - if ! "$PRIVILEGE_TOOL" emerge "$package"; then - failed_packages+=("$package") - any_failures=true - fi - fi + # Setup Git configuration + if command_exists git; then + if [[ "$FORCE_MODE" == true ]] || prompt_user "Configure Git global settings?"; then + local git_name git_email + + if [[ "$FORCE_MODE" != true ]]; then + print_color "$YELLOW" "Enter your Git username: " + read -r git_name + print_color "$YELLOW" "Enter your Git email: " + read -r git_email + else + git_name="${USER}" + git_email="${USER}@$(hostname)" fi - done - ;; - *) - echo "Package manager not supported." - return 1 - ;; - esac - # Check if any packages failed to install - if "$any_failures"; then - echo "Failed to install the following packages:" - printf '%s\n' "${failed_packages[@]}" - return 1 - else - echo "All packages installed successfully." - return 0 + if [[ -n "$git_name" && -n "$git_email" ]]; then + execute_command "git config --global user.name '$git_name'" + execute_command "git config --global user.email '$git_email'" + execute_command "git config --global init.defaultBranch main" + execute_command "git config --global pull.rebase false" + print_success "Git configured with name: $git_name, email: $git_email" + fi + fi fi + + mark_step_completed "setup_development" } -# Install Rust using rustup and install Rust packages from packages.yml -install_rust() { - if command -v "rustup" &>/dev/null; then - echo "Installing Rust using rustup..." - CARGO_HOME=${XDG_DATA_HOME:-$HOME/.local/share}/cargo RUSTUP_HOME=${XDG_DATA_HOME:-$HOME/.local/share}/rustup bash -c 'curl https://sh.rustup.rs -sSf | sh -s -- -y' - else - echo "Rust is already installed." - fi +# Apply system tweaks +apply_tweaks() { + print_section "Applying System Tweaks" + save_state "apply_tweaks" "started" - # Read Rust-specific packages from packages.yml under the 'rust' section - local rust_packages=() - if [[ -f "$packages_file" ]]; then - rust_packages=("$(yq '.rust[]' "$packages_file" 2>/dev/null)") - #rust_packages=("$(grep 'rust:' -A 1 "$packages_file" | grep -v 'rust:' | grep -vE '^\s*$' | sed 's/^\s*-\s*//')") - else - echo "Error: packages.yml not found." - return 1 + if [[ "$CFG_OS" != "linux" ]]; then + print_skip "System tweaks (not supported on $CFG_OS)" + mark_step_completed "apply_tweaks" + return 0 fi - # Install Rust packages using cargo if rust is installed - if command -v "cargo" &>/dev/null; then - for package in "${rust_packages[@]}"; do - if ! cargo install "$package"; then - echo "Failed to install Rust package: $package" - return 1 - fi - done - else - echo "Cargo not found. Rust packages installation skipped." + # Improve system responsiveness + if [[ "$FORCE_MODE" == true ]] || prompt_user "Apply system performance tweaks?"; then + local tweaks_applied=() + + # Swappiness adjustment + if execute_command "echo 'vm.swappiness=10' | $PRIVILEGE_TOOL tee -a /etc/sysctl.conf"; then + tweaks_applied+=("Reduced swappiness to 10") + fi + + # File descriptor limits + if execute_command "echo '$USER soft nofile 65536' | $PRIVILEGE_TOOL tee -a /etc/security/limits.conf"; then + execute_command "echo '$USER hard nofile 65536' | $PRIVILEGE_TOOL tee -a /etc/security/limits.conf" + tweaks_applied+=("Increased file descriptor limits") + fi + + # Apply tweaks immediately where possible + if [[ "$DRY_RUN" != true ]]; then + execute_command "$PRIVILEGE_TOOL sysctl vm.swappiness=10" || true + fi + + if [[ ${#tweaks_applied[@]} -gt 0 ]]; then + print_success "Applied system tweaks:" + for tweak in "${tweaks_applied[@]}"; do + print_info " - $tweak" + done + print_warning "Some tweaks require a reboot to take effect" + fi fi + + mark_step_completed "apply_tweaks" } -# Function to install Node Version Manager (NVM) -install_nvm() { - # Set NVM_DIR environment variable - export NVM_DIR="$HOME/.config/nvm" - if [ ! -d "$NVM_DIR" ]; then - mkdir -p "$NVM_DIR" +#====================================== +# Enhanced Summary and Cleanup +#====================================== + +# Print installation summary +print_installation_summary() { + print_header "Installation Summary" + + # Show progress overview + local total_steps=${#STEP_ORDER[@]} + local completed_count=${#COMPLETED_STEPS[@]} + local failed_count=${#FAILED_ITEMS[@]} + + print_section "Progress Overview" + print_color "$CYAN" "Total Steps: $total_steps" + print_color "$GREEN" "Completed: $completed_count" + print_color "$RED" "Failed: $failed_count" + + local completion_percent=$((completed_count * 100 / total_steps)) + print_color "$BLUE" "Completion: ${completion_percent}%" + + if [[ ${#INSTALL_SUMMARY[@]} -gt 0 ]]; then + print_section "Successful Operations" + printf '%s\n' "${INSTALL_SUMMARY[@]}" fi - # Download and install or update NVM script - if command -v nvm &>/dev/null; then - echo "Updating Node Version Manager (NVM)..." - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash - else - echo "Installing Node Version Manager (NVM)..." - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash + + if [[ ${#SKIPPED_ITEMS[@]} -gt 0 ]]; then + print_section "Skipped Items" + printf '%s\n' "${SKIPPED_ITEMS[@]}" fi - # Source NVM script to enable it in the current shell - if [ -s "$NVM_DIR/nvm.sh" ]; then - echo "Sourcing NVM script..." - . "$NVM_DIR/nvm.sh" + + if [[ ${#FAILED_ITEMS[@]} -gt 0 ]]; then + print_section "Failed Operations" + printf '%s\n' "${FAILED_ITEMS[@]}" + echo + print_warning "Some operations failed. Check the log file: $LOG_FILE" + print_info "Run with --resume to continue from where you left off" else - echo "NVM script not found. Make sure installation was successful." - return 1 + clear_state fi - # Verify installation - if command -v nvm &>/dev/null; then - echo "NVM installation completed successfully." - export NVM_DIR="$HOME/.config/nvm" - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm - [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion + echo + print_color "$GREEN$BOLD" "Installation completed!" + print_info "Log file: $LOG_FILE" "always" + + if [[ ${#FAILED_ITEMS[@]} -eq 0 ]]; then + print_color "$GREEN" "🎉 All operations completed successfully!" else - echo "NVM installation failed." - return 1 + print_color "$YELLOW" "⚠️ Installation completed with ${#FAILED_ITEMS[@]} issues" fi -} -install_node() { - # Check if Node.js is already installed - if ! command -v node &>/dev/null; then - echo "Node.js is already installed." - return + echo + print_section "Next Steps" + print_color "$CYAN" "• Restart your shell or run: exec \$SHELL" + print_color "$CYAN" "• Review configuration files in: $DOTFILES_DIR" + print_color "$CYAN" "• Use 'config status' to manage dotfiles" + + if [[ ${#FAILED_ITEMS[@]} -gt 0 ]]; then + print_color "$YELLOW" "• Run '$0 --resume' to retry failed steps" fi - echo "Installing Node.js..." - # Set up environment variables for Node.js installation - export NVM_NODEJS_ORG_MIRROR=https://npm.taobao.org/mirrors/node/ - export NODEJS_ORG_MIRROR=https://npm.taobao.org/mirrors/node/ + if [[ -d "$BACKUP_DIR" ]] && [[ "$DRY_RUN" != true ]]; then + print_color "$CYAN" "• Backup files saved to: $BACKUP_DIR" + fi + echo +} - # Install the latest stable version of Node.js using NVM - nvm - nvm install node - nvm use node - nvm install --lts - nvm alias default lts/* # Set LTS version as default +#====================================== +# Enhanced Main Installation Flow +#====================================== - echo "Node.js installation completed successfully." -} +# Execute installation step with error handling +execute_step() { + local step_name="$1" + local step_desc="${INSTALLATION_STEPS[$step_name]}" -install_yarn() { - # Check if Yarn is already installed - if command -v yarn &>/dev/null; then - echo "Yarn is already installed." - return - fi + print_section "$step_desc" + save_state "$step_name" "started" - # Check if the .yarn directory exists - if [ -d "$HOME/.yarn" ]; then - echo "Removing existing .yarn directory..." - rm -rf "$HOME/.yarn" + # Skip if already completed and not in force mode + if is_step_completed "$step_name" && [[ "$FORCE_MODE" != true ]]; then + print_success "$step_desc (already completed)" + return 0 fi - echo "Installing Yarn..." - # Install Yarn using npm - curl -o- -L https://yarnpkg.com/install.sh | bash - echo "Yarn installation completed successfully." + # Execute the step function + if "$step_name"; then + print_success "$step_desc completed" + mark_step_completed "$step_name" + return 0 + else + print_error "$step_desc failed" + mark_step_failed "$step_name" + return 1 + fi } -setup_tmux_plugins() { - local tpm_dir="$HOME/.config/tmux/plugins/tpm" - local plugins_dir="$HOME/.config/tmux/plugins" +# Main installation function +main() { + # Parse command line arguments + parse_arguments "$@" - # Ensure the plugins directory exists - if [ ! -d "$plugins_dir" ]; then - mkdir -p "$plugins_dir" - fi + # Initialize + setup_logging "$@" + + print_header "Enhanced Dotfiles Installation" - # Ensure the TPM directory exists - if [ ! -d "$tpm_dir" ]; then - mkdir -p "$tpm_dir" + if [[ "$DRY_RUN" == true ]]; then + print_warning "DRY RUN MODE - No changes will be made" + echo fi - if [ "$(ls -A "$tpm_dir")" ]; then - # TPM is already installed and directory is not empty, so we skip installation. - echo "TPM has been installed...skipping" - else - # If TPM directory doesn't exist or is empty, we proceed with installation. - if [ -d "$tpm_dir" ]; then - rm -rf "$tpm_dir" # Remove existing directory if it exists + print_info "Starting installation for user: $USER" "always" + print_info "Log file: $LOG_FILE" "always" + print_info "Mode: $( + [[ "$RESUME_MODE" == true ]] && echo "Resume" || + [[ "$UPDATE_MODE" == true ]] && echo "Update" || + echo "Fresh Install" + )" "always" + + # Handle resume mode + if [[ "$RESUME_MODE" == true ]]; then + if load_state; then + print_info "Resuming from previous installation..." "always" + print_info "Last step: ${LAST_STEP:-unknown}" "always" + print_info "Step status: ${STEP_STATUS:-unknown}" "always" + + # Load completed steps from state + if [[ -n "${COMPLETED_STEPS:-}" ]]; then + eval "COMPLETED_STEPS=(${COMPLETED_STEPS})" + fi + else + print_warning "No previous installation state found" + print_info "Starting fresh installation..." + RESUME_MODE=false fi - echo "Installing TPM..." - git clone https://github.com/tmux-plugins/tpm "$tpm_dir" fi -} -install_tailscale() { - if ! command -v tailscale &>/dev/null; then - curl -fsSL https://tailscale.com/install.sh | bash - fi -} + # Pre-flight checks + detect_os + detect_privilege_tools -setup_ssh() { - SSH_DIR="$HOME/.ssh" - if ! [[ -f "$SSH_DIR/authorized_keys" ]]; then - echo "Generating SSH keys" - mkdir -p "$SSH_DIR" - chmod 700 "$SSH_DIR" - ssh-keygen -b 4096 -t rsa -f "$SSH_DIR"/id_rsa -N '' -C "$USER@$HOSTNAME" - cat "$SSH_DIR"/id_rsa.pub >>"$SSH_DIR"/authorized_keys + if [[ "$CFG_OS" == "linux" ]]; then + detect_linux_distro || { + print_error "Failed to detect Linux distribution" + exit 1 + } fi -} -linux_specific_steps() { - install_dotfiles - user_dirs - _distro_detect - check_privilege_tools - #set_locale - submodules - change_dir_names - #linux_update_system - install_yq - #install_rust - #install_nvm - #install_node - #install_yarn - install_tailscale - linux_install_packages - install_zsh_plugins - setup_tmux_plugins - setup_ssh - change_shell -} + # System requirements and validation + if ! check_system_requirements; then + if [[ "$FORCE_MODE" != true ]]; then + print_error "System requirements not met" + if ! prompt_user "Continue anyway? (Some features may not work)"; then + exit 1 + fi + fi + fi -#------------------------------------------------------------------------------ + validate_config || print_warning "Configuration validation found issues" -#------------------------------------------------------------------------------ + # Show installation plan + echo + print_color "$YELLOW$BOLD" "Installation Plan:" + local step_number=1 + for step in "${STEP_ORDER[@]}"; do + local step_desc="${INSTALLATION_STEPS[$step]}" + if is_step_completed "$step" && [[ "$FORCE_MODE" != true ]]; then + print_color "$GREEN" "$step_number. $step_desc (✓ completed)" + else + print_color "$CYAN" "$step_number. $step_desc" + fi + step_number=$((step_number + 1)) + done -#============================================================================== + echo + if [[ "$FORCE_MODE" != true ]] && [[ "$DRY_RUN" != true ]] && ! prompt_user "Continue with installation?"; then + print_info "Installation cancelled by user" + exit 0 + fi -#====================================== -# MacOS -#====================================== + # Execute installation steps + local failed_steps=() + local step_number=1 + local total_steps=${#STEP_ORDER[@]} -macos_specific_steps() { - set_locale - macos_install_packages - git_install_macos + for step in "${STEP_ORDER[@]}"; do + echo + print_color "$MAGENTA$BOLD" "[$step_number/$total_steps] ${INSTALLATION_STEPS[$step]}" -} + if execute_step "$step"; then + log_message "INFO" "Step completed successfully: $step" + else + failed_steps+=("$step") + log_message "ERROR" "Step failed: $step" + + # Ask if user wants to continue + if [[ "$FORCE_MODE" != true ]] && [[ "$DRY_RUN" != true ]]; then + echo + if ! prompt_user "Step '$step' failed. Continue with remaining steps?" "Y"; then + print_info "Installation stopped by user" + break + fi + fi + fi -#============================================================================== + step_number=$((step_number + 1)) + done -#====================================== -# Windows -#====================================== + # Post-installation tasks + if [[ ${#failed_steps[@]} -eq 0 ]]; then + print_success "All installation steps completed successfully!" + clear_state + else + print_warning "${#failed_steps[@]} steps failed: ${failed_steps[*]}" + if [[ "${failed_steps[-1]:-}" != "" ]]; then + save_state "${failed_steps[-1]}" "failed" + fi + fi -windows_specific_steps() { - check_git_installed_windows - install_dependencies_windows - windows_install_packages - git_install_windows - symlink_configuration_files_windows -} -#------------------------------------------------------------------------------ + # Show summary + print_installation_summary -#============================================================================== + log_message "INFO" "Installation process completed" -#====================================== -# Main Installation -#====================================== + # Final recommendations + if [[ "$DRY_RUN" != true ]]; then + echo + print_section "Post-Installation Recommendations" + print_color "$CYAN" "• Restart your terminal or run: source ~/.bashrc (or ~/.zshrc)" + print_color "$CYAN" "• Review your dotfiles configuration in: $DOTFILES_DIR" + print_color "$CYAN" "• Use the 'config' command to manage your dotfiles" -# Main Installation -main_installation() { - echo "Starting main installation..." + if [[ ${#failed_steps[@]} -gt 0 ]]; then + print_color "$YELLOW" "• Run '$0 --resume' to retry failed steps" + print_color "$YELLOW" "• Check the log file for detailed error information: $LOG_FILE" + fi - case "$OSTYPE" in - linux-gnu*) - linux_specific_steps - ;; - darwin*) - macos_specific_steps - ;; - msys* | cygwin*) - windows_specific_steps - ;; - *) - handle_error "Unsupported operating system." - ;; - esac + if [[ -d "$BACKUP_DIR" ]]; then + print_color "$CYAN" "• Your original files have been backed up to: $BACKUP_DIR" + fi - sleep 1 -} + echo + print_color "$GREEN$BOLD" "Thank you for using the Enhanced Dotfiles Installation Script!" + fi -# Script entry point -main() { - echo "Log File for Dotfiles Installation" >"$LOG_FILE" - check_download_dependencies - check_os - main_installation - handle_complete "Installation completed successfully." + # Exit with appropriate code + [[ ${#failed_steps[@]} -eq 0 ]] && exit 0 || exit 1 } main "$@" |
