#!/usr/bin/env bash #=============================================================================== # # WSL2 Development Environment Setup Script # # Description : One-click setup to transform WSL2 Ubuntu into a full # development workstation with modern terminal, languages, # and CLI tools. # # Supports : Ubuntu 20.04 / 22.04 / 24.04 / 26.04 # Terminal : Zsh + Zinit (plugin manager) + Powerlevel10k (prompt) # Languages : Go (latest) + Node.js LTS (via fnm) # Proxy : Clash Verge TUN mode compatible # # Usage : sudo bash wsl-dev-setup.sh [OPTIONS] # sudo bash wsl-dev-setup.sh --help # # Version : 1.0.0 # License : MIT # #=============================================================================== set -uo pipefail #=============================================================================== # Global Configuration #=============================================================================== readonly SCRIPT_VERSION="1.0.0" readonly SCRIPT_NAME="$(basename "$0")" readonly SCRIPT_START_TIME="$(date +%s)" # --- Network / Proxy --- WINDOWS_HOST_IP="${WINDOWS_HOST_IP:-192.168.1.20}" CLASH_PROXY_PORT="${CLASH_PROXY_PORT:-7890}" ENABLE_PROXY="${ENABLE_PROXY:-true}" # --- User --- INSTALL_USER="${SUDO_USER:-$(whoami)}" USER_HOME="$(eval echo ~"${INSTALL_USER}" 2>/dev/null || echo "/home/${INSTALL_USER}")" # --- Installation Versions --- GO_VERSION="${GO_VERSION:-latest}" NODE_VERSION="${NODE_VERSION:-lts}" NERD_FONT="${NERD_FONT:-JetBrainsMono}" # --- APT Mirror --- APT_MIRROR="${APT_MIRROR:-tuna}" # tuna | ustc | aliyun | default # --- Detected at runtime --- UBUNTU_VERSION="" UBUNTU_CODENAME="" ARCH="" GO_ARCH="" IS_WSL=false WIN_USER="" # --- Module control --- ALL_MODULES="proxy mirror locale base-tools zsh fonts modern-cli golang nodejs" SKIP_MODULES="" ONLY_MODULES="" FAILED_MODULES="" #=============================================================================== # Colors & Logging #=============================================================================== if [[ -t 1 ]]; then readonly C_RED='\033[0;31m' readonly C_GREEN='\033[0;32m' readonly C_YELLOW='\033[0;33m' readonly C_BLUE='\033[0;34m' readonly C_MAGENTA='\033[0;35m' readonly C_CYAN='\033[0;36m' readonly C_WHITE='\033[1;37m' readonly C_BOLD='\033[1m' readonly C_DIM='\033[2m' readonly C_NC='\033[0m' else readonly C_RED='' C_GREEN='' C_YELLOW='' C_BLUE='' readonly C_MAGENTA='' C_CYAN='' C_WHITE='' C_BOLD='' C_DIM='' C_NC='' fi log_info() { echo -e "${C_BLUE}[INFO]${C_NC} $*"; } log_success() { echo -e "${C_GREEN}[ OK ]${C_NC} $*"; } log_warn() { echo -e "${C_YELLOW}[WARN]${C_NC} $*"; } log_error() { echo -e "${C_RED}[ERROR]${C_NC} $*"; } log_step() { echo -e "${C_MAGENTA} ➜${C_NC} $*"; } section_header() { local title="$1" local width=58 echo "" echo -e "${C_CYAN}┌$(printf '─%.0s' $(seq 1 $width))┐${C_NC}" printf "${C_CYAN}│${C_NC} %-$((width - 2))s${C_CYAN}│${C_NC}\n" "$title" echo -e "${C_CYAN}└$(printf '─%.0s' $(seq 1 $width))┘${C_NC}" echo "" } print_banner() { echo "" echo -e "${C_CYAN}╔══════════════════════════════════════════════════════════════╗${C_NC}" echo -e "${C_CYAN}║${C_NC} ${C_BOLD}🚀 WSL2 Development Environment Setup${C_NC} v${SCRIPT_VERSION} ${C_CYAN}║${C_NC}" echo -e "${C_CYAN}║${C_NC} ${C_DIM}Zinit + Powerlevel10k · Go · Node.js · Modern CLI${C_NC} ${C_CYAN}║${C_NC}" echo -e "${C_CYAN}╚══════════════════════════════════════════════════════════════╝${C_NC}" echo "" } #=============================================================================== # Usage / Help #=============================================================================== print_usage() { cat << USAGE ${C_BOLD}WSL2 Development Environment Setup Script${C_NC} v${SCRIPT_VERSION} ${C_BOLD}USAGE:${C_NC} sudo bash ${SCRIPT_NAME} [OPTIONS] ${C_BOLD}OPTIONS:${C_NC} -h, --help Show this help message -v, --version Show script version --only Only run specified modules (comma-separated) --skip Skip specified modules (comma-separated) --no-proxy Disable proxy configuration --proxy-ip Windows host IP (default: 192.168.1.20) --proxy-port Clash proxy port (default: 7890) --mirror APT mirror: tuna|ustc|aliyun|default (default: tuna) --go-version Go version (default: latest) --node-version Node.js version (default: lts) --font Nerd Font name (default: JetBrainsMono) --dry-run Show what would be done without executing ${C_BOLD}MODULES:${C_NC} proxy Proxy configuration (Clash Verge) mirror APT mirror source configuration locale Locale & UTF-8 setup base-tools Essential system tools zsh Zsh + Zinit + Powerlevel10k fonts Nerd Font installation modern-cli Modern CLI replacements (bat, eza, fzf, etc.) golang Go programming language nodejs Node.js via fnm ${C_BOLD}EXAMPLES:${C_NC} # Full installation (recommended) sudo bash ${SCRIPT_NAME} # Install only Go and Node.js sudo bash ${SCRIPT_NAME} --only golang,nodejs # Skip fonts and modern CLI tools sudo bash ${SCRIPT_NAME} --skip fonts,modern-cli # Use USTC mirror with custom proxy IP sudo bash ${SCRIPT_NAME} --mirror ustc --proxy-ip 192.168.1.100 ${C_BOLD}ENVIRONMENT VARIABLES:${C_NC} WINDOWS_HOST_IP Windows host IP address CLASH_PROXY_PORT Clash proxy port ENABLE_PROXY Enable proxy (true/false) GO_VERSION Go version to install NODE_VERSION Node.js version to install APT_MIRROR APT mirror name USAGE } #=============================================================================== # Argument Parsing #=============================================================================== DRY_RUN=false parse_args() { while [[ $# -gt 0 ]]; do case "$1" in -h|--help) print_usage; exit 0 ;; -v|--version) echo "${SCRIPT_NAME} v${SCRIPT_VERSION}"; exit 0 ;; --only) ONLY_MODULES="${2//,/ }"; shift 2 ;; --skip) SKIP_MODULES="${2//,/ }"; shift 2 ;; --no-proxy) ENABLE_PROXY="false"; shift ;; --proxy-ip) WINDOWS_HOST_IP="$2"; shift 2 ;; --proxy-port) CLASH_PROXY_PORT="$2"; shift 2 ;; --mirror) APT_MIRROR="$2"; shift 2 ;; --go-version) GO_VERSION="$2"; shift 2 ;; --node-version) NODE_VERSION="$2"; shift 2 ;; --font) NERD_FONT="$2"; shift 2 ;; --dry-run) DRY_RUN=true; shift ;; *) log_error "Unknown option: $1" print_usage exit 1 ;; esac done } #=============================================================================== # Utility Functions #=============================================================================== # Check if a command exists check_cmd() { command -v "$1" &>/dev/null } # Run command as the installation user (not root) run_as_user() { local cmd="$1" if [[ "$(id -u)" -eq 0 ]] && [[ "${INSTALL_USER}" != "root" ]]; then su "${INSTALL_USER}" -s /bin/bash -c " export HOME='${USER_HOME}' export http_proxy='${http_proxy:-}' export https_proxy='${https_proxy:-}' export HTTP_PROXY='${HTTP_PROXY:-}' export HTTPS_PROXY='${HTTPS_PROXY:-}' export no_proxy='${no_proxy:-}' export NO_PROXY='${NO_PROXY:-}' export PATH='${PATH}' cd '${USER_HOME}' ${cmd} " else eval "${cmd}" fi } # Backup a file before modification backup_file() { local file="$1" if [[ -f "$file" ]]; then local backup="${file}.bak.$(date +%Y%m%d%H%M%S)" cp "$file" "$backup" log_step "Backed up: $(basename "$file") → $(basename "$backup")" fi } # Add a block to a shell config file (idempotent) add_to_rc() { local file="$1" local marker="$2" local content="$3" # Create file if it doesn't exist if [[ ! -f "$file" ]]; then touch "$file" chown "${INSTALL_USER}:${INSTALL_USER}" "$file" 2>/dev/null || true fi # Remove old block if exists, then add new if grep -q "# >>> ${marker} >>>" "$file" 2>/dev/null; then # Remove old block sed -i "/# >>> ${marker} >>>/,/# <<< ${marker} <<>> ${marker} >>>" echo "${content}" echo "# <<< ${marker} <<<" } >> "$file" } # Compare versions: returns 0 if $1 >= $2 version_gte() { [[ "$(printf '%s\n' "$1" "$2" | sort -V | head -n1)" == "$2" ]] } # Ensure required commands are available ensure_deps() { local missing=() local check_cmd_name for dep in "$@"; do check_cmd_name="$dep" if [[ "$dep" == "fontconfig" ]]; then check_cmd_name="fc-cache" fi if ! check_cmd "$check_cmd_name"; then missing+=("$dep") fi done if [[ ${#missing[@]} -gt 0 ]]; then log_step "Installing dependencies: ${missing[*]}" if ! apt-get install -y -qq "${missing[@]}" >/dev/null 2>&1; then log_error "Failed to install dependencies: ${missing[*]}" return 1 fi # Double check if dependencies were successfully installed local still_missing=() for dep in "${missing[@]}"; do check_cmd_name="$dep" if [[ "$dep" == "fontconfig" ]]; then check_cmd_name="fc-cache" fi if ! check_cmd "$check_cmd_name"; then still_missing+=("$dep") fi done if [[ ${#still_missing[@]} -gt 0 ]]; then log_error "Dependencies still missing after installation attempt: ${still_missing[*]}" return 1 fi fi return 0 } # Download a GitHub release asset download_github_asset() { local repo="$1" local pattern="$2" local output="$3" ensure_deps curl jq || return 1 local api_url="https://api.github.com/repos/${repo}/releases/latest" local url url=$(curl -sL --connect-timeout 20 "${api_url}" \ | jq -r --arg pat "${pattern}" '.assets[] | select(.name | test($pat; "i")) | .browser_download_url' \ | head -1) if [[ -z "$url" || "$url" == "null" ]]; then log_error "Failed to find GitHub asset: ${repo} matching '${pattern}'" return 1 fi log_step "Downloading: $(basename "$url")" curl -fsSL --connect-timeout 30 --retry 3 -L "$url" -o "$output" } # Install a .deb package from GitHub releases install_github_deb() { local repo="$1" local pattern="$2" local tmp_deb tmp_deb=$(mktemp /tmp/wsl-setup-XXXXXX.deb) download_github_asset "$repo" "$pattern" "$tmp_deb" || return 1 dpkg -i "$tmp_deb" >/dev/null 2>&1 || apt-get install -f -y -qq >/dev/null 2>&1 rm -f "$tmp_deb" } # Install a binary from a GitHub release tarball install_github_tar_binary() { local repo="$1" local pattern="$2" local binary_name="$3" local tmp_dir tmp_dir=$(mktemp -d /tmp/wsl-setup-XXXXXX) local tmp_file="${tmp_dir}/download.tar.gz" download_github_asset "$repo" "$pattern" "$tmp_file" || { rm -rf "$tmp_dir"; return 1; } tar -xzf "$tmp_file" -C "$tmp_dir" 2>/dev/null local found found=$(find "$tmp_dir" -name "$binary_name" -type f 2>/dev/null | head -1) if [[ -n "$found" ]]; then install -m 755 "$found" "/usr/local/bin/${binary_name}" else log_error "Binary '${binary_name}' not found in archive" rm -rf "$tmp_dir" return 1 fi rm -rf "$tmp_dir" } # Check if a module should run should_run_module() { local module="$1" if [[ -n "$ONLY_MODULES" ]]; then [[ " $ONLY_MODULES " == *" $module "* ]] else [[ " $SKIP_MODULES " != *" $module "* ]] fi } #=============================================================================== # System Detection #=============================================================================== detect_system() { section_header "🖥️ System Detection" # --- OS Detection --- if [[ ! -f /etc/os-release ]]; then log_error "/etc/os-release not found — is this Ubuntu?" exit 1 fi # shellcheck source=/dev/null source /etc/os-release UBUNTU_VERSION="${VERSION_ID:-unknown}" UBUNTU_CODENAME="${VERSION_CODENAME:-unknown}" # Fallback codename detection if [[ "$UBUNTU_CODENAME" == "unknown" ]]; then UBUNTU_CODENAME=$(lsb_release -cs 2>/dev/null || echo "unknown") fi case "$UBUNTU_VERSION" in 20.04) UBUNTU_CODENAME="focal" ;; 22.04) UBUNTU_CODENAME="jammy" ;; 24.04) UBUNTU_CODENAME="noble" ;; 26.04) : ;; # Use auto-detected codename esac case "$UBUNTU_VERSION" in 20.04|22.04|24.04|26.04) log_success "Ubuntu ${UBUNTU_VERSION} (${UBUNTU_CODENAME}) — Supported ✓" ;; *) log_warn "Ubuntu ${UBUNTU_VERSION} (${UBUNTU_CODENAME}) — Not officially tested" log_warn "Will attempt best-effort compatibility" ;; esac # --- Architecture --- ARCH=$(dpkg --print-architecture 2>/dev/null || echo "amd64") case "$ARCH" in amd64) GO_ARCH="amd64" ;; arm64) GO_ARCH="arm64" ;; *) log_error "Unsupported architecture: ${ARCH}"; exit 1 ;; esac log_info "Architecture: ${ARCH}" # --- WSL Detection --- if grep -qi microsoft /proc/version 2>/dev/null; then IS_WSL=true log_info "Environment: WSL2" WIN_USER=$(cmd.exe /c "echo %USERNAME%" 2>/dev/null | tr -d '\r\n' || echo "") if [[ -n "$WIN_USER" ]]; then log_info "Windows user: ${WIN_USER}" fi else IS_WSL=false log_info "Environment: Native Linux" fi # --- User --- log_info "Install user: ${INSTALL_USER} (HOME=${USER_HOME})" } #=============================================================================== # Proxy Configuration #=============================================================================== setup_proxy() { section_header "🌐 Proxy Configuration (Clash Verge)" local proxy_http="http://${WINDOWS_HOST_IP}:${CLASH_PROXY_PORT}" local proxy_socks="socks5://${WINDOWS_HOST_IP}:${CLASH_PROXY_PORT}" local no_proxy_list="localhost,127.0.0.1,::1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12" log_info "HTTP proxy: ${proxy_http}" log_info "SOCKS proxy: ${proxy_socks}" # 1. Unset any proxy environment variables first to test direct connectivity unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY log_step "Testing direct connectivity (checking if TUN mode is handling traffic)..." local direct_ok=false if curl -sI --connect-timeout 5 https://github.com >/dev/null 2>&1 || curl -sI --connect-timeout 5 https://www.google.com >/dev/null 2>&1; then direct_ok=true log_success "Direct connection working (TUN mode or native routing active) ✓" fi local proxy_ok=false if [[ "$direct_ok" == "false" ]]; then log_step "Direct connection unavailable. Testing manual proxy connectivity..." export http_proxy="$proxy_http" export https_proxy="$proxy_http" export HTTP_PROXY="$proxy_http" export HTTPS_PROXY="$proxy_http" export all_proxy="$proxy_socks" export ALL_PROXY="$proxy_socks" export no_proxy="$no_proxy_list" export NO_PROXY="$no_proxy_list" if curl -sI --connect-timeout 5 https://github.com >/dev/null 2>&1 || curl -sI --connect-timeout 5 https://www.google.com >/dev/null 2>&1; then proxy_ok=true log_success "Proxy connection working ✓" else log_warn "Proxy connectivity test failed — proxy port may be unreachable" # Unset broken proxy variables so we don't break subsequent commands unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY fi fi # --- Persist proxy_on/proxy_off to .bashrc and .profile --- local proxy_shell_block proxy_shell_block=$(cat << EOFPROXYBLOCK proxy_on() { export http_proxy="${proxy_http}" export https_proxy="${proxy_http}" export HTTP_PROXY="\${http_proxy}" export HTTPS_PROXY="\${https_proxy}" export all_proxy="${proxy_socks}" export ALL_PROXY="\${all_proxy}" export no_proxy="${no_proxy_list}" export NO_PROXY="\${no_proxy}" echo "🌐 Proxy ON: ${proxy_http}" } proxy_off() { unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY echo "🚫 Proxy OFF" } EOFPROXYBLOCK ) if [[ "$proxy_ok" == "true" ]]; then proxy_shell_block+=$'\n\n# Auto-enable proxy on shell start\nproxy_on >/dev/null 2>&1' # Git proxy run_as_user "git config --global http.proxy '${proxy_http}'" 2>/dev/null || true run_as_user "git config --global https.proxy '${proxy_http}'" 2>/dev/null || true AUTO_ENABLE_PROXY="true" log_success "Proxy configured and enabled (auto-enable on shell start)" else proxy_shell_block+=$'\n\n# Proxy auto-enable skipped (tested offline or TUN mode active)' # Remove git proxy since direct mode is used or proxy is unreachable run_as_user "git config --global --unset http.proxy" 2>/dev/null || true run_as_user "git config --global --unset https.proxy" 2>/dev/null || true AUTO_ENABLE_PROXY="false" if [[ "$direct_ok" == "true" ]]; then log_success "Using transparent direct routing (helper functions written to shell rc)" else log_warn "Network seems offline — proxy configuration written but disabled" fi fi add_to_rc "${USER_HOME}/.bashrc" "WSL-PROXY" "$proxy_shell_block" add_to_rc "${USER_HOME}/.profile" "WSL-PROXY" "$proxy_shell_block" } #=============================================================================== # APT Mirror Configuration #=============================================================================== setup_apt_mirror() { section_header "📦 APT Mirror Configuration" local mirror_url case "$APT_MIRROR" in tuna) mirror_url="https://mirrors.tuna.tsinghua.edu.cn" ;; ustc) mirror_url="https://mirrors.ustc.edu.cn" ;; aliyun) mirror_url="https://mirrors.aliyun.com" ;; default) log_info "Using default APT sources — skipping"; return 0 ;; *) log_warn "Unknown mirror '${APT_MIRROR}', falling back to tuna" mirror_url="https://mirrors.tuna.tsinghua.edu.cn" ;; esac log_info "Mirror: ${mirror_url}" log_info "Codename: ${UBUNTU_CODENAME}" if version_gte "$UBUNTU_VERSION" "24.04"; then _setup_apt_deb822 "$mirror_url" else _setup_apt_traditional "$mirror_url" fi log_step "Updating package lists..." apt-get update -qq >/dev/null 2>&1 log_success "APT mirror configured ✓" } _setup_apt_traditional() { local mirror_url="$1" local sources_file="/etc/apt/sources.list" backup_file "$sources_file" cat > "$sources_file" << EOF # Ubuntu APT Sources — Generated by wsl-dev-setup.sh # Mirror: ${mirror_url} # Date: $(date -Iseconds) deb ${mirror_url}/ubuntu/ ${UBUNTU_CODENAME} main restricted universe multiverse deb ${mirror_url}/ubuntu/ ${UBUNTU_CODENAME}-updates main restricted universe multiverse deb ${mirror_url}/ubuntu/ ${UBUNTU_CODENAME}-backports main restricted universe multiverse deb ${mirror_url}/ubuntu/ ${UBUNTU_CODENAME}-security main restricted universe multiverse # deb-src ${mirror_url}/ubuntu/ ${UBUNTU_CODENAME} main restricted universe multiverse # deb-src ${mirror_url}/ubuntu/ ${UBUNTU_CODENAME}-updates main restricted universe multiverse # deb-src ${mirror_url}/ubuntu/ ${UBUNTU_CODENAME}-backports main restricted universe multiverse # deb-src ${mirror_url}/ubuntu/ ${UBUNTU_CODENAME}-security main restricted universe multiverse EOF log_step "Wrote traditional sources.list" } _setup_apt_deb822() { local mirror_url="$1" local sources_file="/etc/apt/sources.list.d/ubuntu.sources" backup_file "$sources_file" # Disable legacy sources.list if it exists [[ -f /etc/apt/sources.list ]] && \ mv /etc/apt/sources.list /etc/apt/sources.list.disabled 2>/dev/null || true cat > "$sources_file" << EOF ## Ubuntu APT Sources (DEB822) — Generated by wsl-dev-setup.sh ## Mirror: ${mirror_url} ## Date: $(date -Iseconds) Types: deb URIs: ${mirror_url}/ubuntu/ Suites: ${UBUNTU_CODENAME} ${UBUNTU_CODENAME}-updates ${UBUNTU_CODENAME}-backports Components: main restricted universe multiverse Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg Types: deb URIs: ${mirror_url}/ubuntu/ Suites: ${UBUNTU_CODENAME}-security Components: main restricted universe multiverse Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg EOF log_step "Wrote DEB822 format ubuntu.sources" } #=============================================================================== # Locale Configuration #=============================================================================== setup_locale() { section_header "🌍 Locale & UTF-8 Configuration" if ! apt-get install -y -qq locales >/dev/null 2>&1; then log_warn "Failed to install locales package. Skipping locale configuration." return 0 fi sed -i 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen 2>/dev/null || true sed -i 's/# zh_CN.UTF-8 UTF-8/zh_CN.UTF-8 UTF-8/' /etc/locale.gen 2>/dev/null || true if locale-gen en_US.UTF-8 zh_CN.UTF-8 >/dev/null 2>&1; then update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LANGUAGE=en_US:en 2>/dev/null || true export LANG=en_US.UTF-8 export LC_ALL=en_US.UTF-8 log_success "Locale: en_US.UTF-8 + zh_CN.UTF-8 ✓" else log_warn "locale-gen failed to generate locales" fi } #=============================================================================== # Base Tools Installation #=============================================================================== install_base_tools() { section_header "🛠️ Base Tools Installation" # Common packages for all versions local packages=( # Core curl wget git # Network diagnostics net-tools iputils-ping mtr-tiny dnsutils traceroute # Build essentials build-essential gcc g++ make cmake pkg-config # Security / certificates ca-certificates gnupg lsb-release software-properties-common apt-transport-https # Compression unzip zip tar gzip bzip2 xz-utils # JSON / text jq tree # System monitoring htop ncdu # SSH openssh-client # Terminal multiplexer tmux # Fonts fontconfig ) # telnet: package name varies by version if apt-cache show inetutils-telnet >/dev/null 2>&1; then packages+=(inetutils-telnet) elif apt-cache show telnet >/dev/null 2>&1; then packages+=(telnet) fi # p7zip if apt-cache show p7zip-full >/dev/null 2>&1; then packages+=(p7zip-full) fi log_info "Installing ${#packages[@]} packages..." apt-get install -y -qq "${packages[@]}" >/dev/null 2>&1 # Verify key tools local tools_ok=true for tool in curl git wget ping mtr; do if check_cmd "$tool"; then log_step "${tool} ✓" else log_warn "${tool} — not found after install" tools_ok=false fi done # Check telnet specifically (might be inetutils-telnet) if check_cmd telnet; then log_step "telnet ✓" else log_warn "telnet — not found (try: apt install telnet)" fi if $tools_ok; then log_success "Base tools installed ✓" else log_warn "Some tools may require manual installation" fi } #=============================================================================== # Zsh + Zinit + Powerlevel10k #=============================================================================== install_zsh_terminal() { section_header "💻 Zsh + Zinit + Powerlevel10k" # --- Install Zsh --- if ! check_cmd zsh; then log_step "Installing Zsh..." if ! apt-get install -y -qq zsh >/dev/null 2>&1; then log_error "Failed to install Zsh" return 1 fi fi if ! check_cmd zsh; then log_error "Zsh command not found after installation attempt" return 1 fi log_success "Zsh $(zsh --version 2>/dev/null | head -c 20) ✓" # --- Install Zinit --- local zinit_home="${USER_HOME}/.local/share/zinit/zinit.git" if [[ ! -d "$zinit_home" ]]; then log_step "Installing Zinit plugin manager..." run_as_user "mkdir -p '${USER_HOME}/.local/share/zinit'" if ! run_as_user "git clone --depth=1 https://github.com/zdharma-continuum/zinit.git '${zinit_home}'"; then log_error "Failed to clone Zinit plugin manager from GitHub" return 1 fi log_success "Zinit installed ✓" else log_info "Zinit already installed — pulling updates..." run_as_user "cd '${zinit_home}' && git pull --quiet" || true fi # --- Generate .zshrc --- _generate_zshrc # --- Generate .p10k.zsh --- _generate_p10k # --- Set Zsh as default shell --- local zsh_path zsh_path=$(which zsh) if [[ -n "$zsh_path" ]]; then local current_shell current_shell=$(getent passwd "${INSTALL_USER}" 2>/dev/null | cut -d: -f7) if [[ "$current_shell" != "$zsh_path" ]]; then if chsh -s "$zsh_path" "$INSTALL_USER" 2>/dev/null; then log_success "Default shell → Zsh ✓" else log_warn "Failed to set default shell to Zsh using chsh" fi else log_info "Zsh already set as default shell" fi else log_warn "Could not determine Zsh path — skipping shell change" fi } _generate_zshrc() { local zshrc="${USER_HOME}/.zshrc" backup_file "$zshrc" cat > "$zshrc" << 'EOFZSHRC' # Ensure UTF-8 locale is fully active at the very start of the shell session # (Important for CJK character width calculations in P10k and fzf-tab) export LANG=en_US.UTF-8 export LC_ALL=en_US.UTF-8 export LC_CTYPE=en_US.UTF-8 # ╔══════════════════════════════════════════════════════════════╗ # ║ Zsh Configuration ║ # ║ Powered by Zinit + Powerlevel10k ║ # ║ Generated by wsl-dev-setup.sh ║ # ╚══════════════════════════════════════════════════════════════╝ # ┌──────────────────────────────────────────┐ # │ Powerlevel10k Instant Prompt │ # │ (must stay near the top of .zshrc) │ # └──────────────────────────────────────────┘ if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" fi # ┌──────────────────────────────────────────┐ # │ Zinit Initialization │ # └──────────────────────────────────────────┘ ZINIT_HOME="${HOME}/.local/share/zinit/zinit.git" if [[ -f "${ZINIT_HOME}/zinit.zsh" ]]; then source "${ZINIT_HOME}/zinit.zsh" autoload -Uz _zinit (( ${+_comps} )) && _comps[zinit]=_zinit fi # ┌──────────────────────────────────────────┐ # │ Completion System Setup │ # └──────────────────────────────────────────┘ # Initialize compinit synchronously so completion functions and plugins (like fzf-tab) # hook into the completion system correctly and reliably. mkdir -p "${HOME}/.cache" autoload -Uz compinit compinit -d "${HOME}/.cache/zcompdump" # ┌──────────────────────────────────────────┐ # │ Theme: Powerlevel10k │ # └──────────────────────────────────────────┘ zinit ice depth=1 zinit light romkatv/powerlevel10k # ┌──────────────────────────────────────────┐ # │ Plugins (Zinit Turbo Mode) │ # └──────────────────────────────────────────┘ # Core plugins — loaded asynchronously for fast startup zinit wait lucid for \ atinit"ZINIT[COMPINIT_OPTS]=-C; zicompinit; zicdreplay" \ zdharma-continuum/fast-syntax-highlighting \ blockf \ zsh-users/zsh-completions \ atload"!_zsh_autosuggest_start" \ zsh-users/zsh-autosuggestions # History substring search (↑↓ arrow key filtering) # Loaded synchronously so widgets are registered before keybindings and syntax highlighting zinit light zsh-users/zsh-history-substring-search # fzf-tab: replace default tab completion with fzf zinit wait lucid for \ Aloxaf/fzf-tab # Oh-My-Zsh snippets (cherry-picked essentials, NOT the whole framework) zinit wait lucid for \ OMZL::history.zsh \ OMZL::key-bindings.zsh \ OMZL::completion.zsh \ OMZL::directories.zsh \ OMZP::git \ OMZP::sudo \ OMZP::extract \ OMZP::command-not-found # ┌──────────────────────────────────────────┐ # │ History │ # └──────────────────────────────────────────┘ HISTFILE="${HOME}/.zsh_history" HISTSIZE=50000 SAVEHIST=50000 setopt EXTENDED_HISTORY # Record timestamp setopt HIST_EXPIRE_DUPS_FIRST # Expire duplicates first setopt HIST_IGNORE_DUPS # Ignore contiguous duplicates setopt HIST_IGNORE_ALL_DUPS # Remove older duplicates setopt HIST_IGNORE_SPACE # Ignore commands starting with space setopt HIST_FIND_NO_DUPS # No duplicates in search setopt HIST_SAVE_NO_DUPS # No duplicates on save setopt SHARE_HISTORY # Share history across sessions setopt INC_APPEND_HISTORY # Append immediately # ┌──────────────────────────────────────────┐ # │ Completion │ # └──────────────────────────────────────────┘ zstyle ':completion:*' matcher-list \ 'm:{a-zA-Z}={A-Za-z}' \ 'r:|[._-]=* r:|=*' zstyle ':completion:*' list-colors "${(s.:.)LS_COLORS}" zstyle ':completion:*' menu no zstyle ':completion:*:descriptions' format '[%d]' zstyle ':completion:*' special-dirs true zstyle ':fzf-tab:complete:cd:*' fzf-preview \ 'eza -1 --color=always --icons $realpath 2>/dev/null || ls -1 --color=always $realpath 2>/dev/null' zstyle ':fzf-tab:*' switch-group '<' '>' # ┌──────────────────────────────────────────┐ # │ Key Bindings │ # └──────────────────────────────────────────┘ bindkey '^[[A' history-substring-search-up 2>/dev/null bindkey '^[[B' history-substring-search-down 2>/dev/null bindkey '^P' history-substring-search-up 2>/dev/null bindkey '^N' history-substring-search-down 2>/dev/null bindkey '^[OA' history-substring-search-up 2>/dev/null bindkey '^[OB' history-substring-search-down 2>/dev/null # ┌──────────────────────────────────────────┐ # │ Aliases │ # └──────────────────────────────────────────┘ # ── Modern tool replacements ── if command -v eza &>/dev/null; then alias ls='eza --icons --group-directories-first' alias ll='eza -alh --icons --group-directories-first --git' alias la='eza -a --icons --group-directories-first' alias lt='eza --tree --icons --level=3' else alias ll='ls -alh --color=auto' alias la='ls -A --color=auto' fi if command -v bat &>/dev/null; then alias cat='bat --paging=never --style=plain' alias catn='bat --paging=never' elif command -v batcat &>/dev/null; then alias cat='batcat --paging=never --style=plain' alias catn='batcat --paging=never' fi command -v fdfind &>/dev/null && ! command -v fd &>/dev/null && alias fd='fdfind' # ── Navigation ── alias ..='cd ..' alias ...='cd ../..' alias ....='cd ../../..' # ── Utilities ── alias cls='clear' alias ports='ss -tulnp' alias myip='curl -s ifconfig.me && echo' alias df='df -h' alias free='free -h' alias grep='grep --color=auto' alias mkdir='mkdir -pv' alias reload='exec zsh' # ── Git extras ── alias glog='git log --oneline --graph --decorate -20' alias gd='git diff' alias gs='git status -sb' # ┌──────────────────────────────────────────┐ # │ Environment — Go │ # └──────────────────────────────────────────┘ if [[ -d /usr/local/go ]]; then export GOROOT=/usr/local/go export GOPATH="${HOME}/go" export PATH="${GOROOT}/bin:${GOPATH}/bin:${PATH}" export GOPROXY=https://goproxy.cn,direct export GONOSUMDB="*" fi # ┌──────────────────────────────────────────┐ # │ Environment — fnm (Node.js) │ # └──────────────────────────────────────────┘ if [[ -d "${HOME}/.local/share/fnm" ]]; then export FNM_DIR="${HOME}/.local/share/fnm" export PATH="${FNM_DIR}:${PATH}" fi if command -v fnm &>/dev/null; then eval "$(fnm env --use-on-cd --shell zsh)" fi # ┌──────────────────────────────────────────┐ # │ Tool Integrations │ # └──────────────────────────────────────────┘ # fzf if [[ -f "${HOME}/.fzf.zsh" ]]; then source "${HOME}/.fzf.zsh" elif command -v fzf &>/dev/null; then eval "$(fzf --zsh 2>/dev/null)" || true fi # zoxide (smart cd) if command -v zoxide &>/dev/null; then eval "$(zoxide init zsh)" fi # delta as git pager if command -v delta &>/dev/null; then export GIT_PAGER='delta' fi # ┌──────────────────────────────────────────┐ # │ Powerlevel10k Config │ # └──────────────────────────────────────────┘ # To customize prompt, run `p10k configure` or edit ~/.p10k.zsh [[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh EOFZSHRC # --- Append proxy config if enabled --- if [[ "${ENABLE_PROXY}" == "true" ]]; then cat >> "$zshrc" << EOFPROXY # >>> WSL-PROXY >>> proxy_on() { export http_proxy="http://${WINDOWS_HOST_IP}:${CLASH_PROXY_PORT}" export https_proxy="http://${WINDOWS_HOST_IP}:${CLASH_PROXY_PORT}" export HTTP_PROXY="\${http_proxy}" export HTTPS_PROXY="\${https_proxy}" export all_proxy="socks5://${WINDOWS_HOST_IP}:${CLASH_PROXY_PORT}" export ALL_PROXY="\${all_proxy}" export no_proxy="localhost,127.0.0.1,::1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12" export NO_PROXY="\${no_proxy}" echo "🌐 Proxy ON: http://${WINDOWS_HOST_IP}:${CLASH_PROXY_PORT}" } proxy_off() { unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY echo "🚫 Proxy OFF" } EOFPROXY if [[ "${AUTO_ENABLE_PROXY:-false}" == "true" ]]; then cat >> "$zshrc" << EOFPROXY_AUTO # Auto-enable proxy proxy_on >/dev/null 2>&1 # <<< WSL-PROXY <<< EOFPROXY_AUTO else cat >> "$zshrc" << EOFPROXY_AUTO # Proxy auto-enable skipped (tested offline or TUN mode active) # <<< WSL-PROXY <<< EOFPROXY_AUTO fi fi chown "${INSTALL_USER}:${INSTALL_USER}" "$zshrc" log_success ".zshrc generated ✓" } _generate_p10k() { local p10k_file="${USER_HOME}/.p10k.zsh" log_step "Generating Powerlevel10k config (Rainbow style)..." cat > "$p10k_file" << 'EOFP10K' # ╔══════════════════════════════════════════════════════════════╗ # ║ Powerlevel10k Configuration ║ # ║ Style: Rainbow with Nerd Font v3 ║ # ║ Generated by wsl-dev-setup.sh ║ # ║ To reconfigure interactively: p10k configure ║ # ╚══════════════════════════════════════════════════════════════╝ 'builtin' 'local' '-a' 'p10k_config_opts' [[ ! -o 'aliases' ]] || p10k_config_opts+=('aliases') [[ ! -o 'sh_glob' ]] || p10k_config_opts+=('sh_glob') [[ ! -o 'no_brace_expand' ]] || p10k_config_opts+=('no_brace_expand') 'builtin' 'setopt' 'no_aliases' 'no_sh_glob' 'brace_expand' () { emulate -L zsh -o extended_glob # Reset all P10k settings unset -m '(POWERLEVEL9K_*|DEFAULT_USER)~POWERLEVEL9K_GITSTATUS_DIR' # ══════════════════════════════════════ # Prompt Layout # ══════════════════════════════════════ typeset -g POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=( os_icon # OS icon (Ubuntu/Linux) dir # Current directory vcs # Git status newline # ── new line ── prompt_char # ❯ or ✘ ) typeset -g POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=( status # Exit code on error command_execution_time # Duration of last command background_jobs # Background job indicator direnv # direnv status go_version # Go version (in Go projects) node_version # Node.js version (in Node projects) context # user@hostname (SSH only) time # Current time HH:MM ) # ══════════════════════════════════════ # General # ══════════════════════════════════════ typeset -g POWERLEVEL9K_MODE=nerdfont-v3 typeset -g POWERLEVEL9K_ICON_PADDING=moderate typeset -g POWERLEVEL9K_ICON_BEFORE_CONTENT= typeset -g POWERLEVEL9K_PROMPT_ADD_NEWLINE=true # Powerline separators typeset -g POWERLEVEL9K_LEFT_SEGMENT_SEPARATOR='\uE0B0' typeset -g POWERLEVEL9K_RIGHT_SEGMENT_SEPARATOR='\uE0B2' typeset -g POWERLEVEL9K_LEFT_SUBSEGMENT_SEPARATOR='\uE0B1' typeset -g POWERLEVEL9K_RIGHT_SUBSEGMENT_SEPARATOR='\uE0B3' typeset -g POWERLEVEL9K_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL='\uE0B0' typeset -g POWERLEVEL9K_RIGHT_PROMPT_FIRST_SEGMENT_START_SYMBOL='\uE0B2' typeset -g POWERLEVEL9K_LEFT_PROMPT_FIRST_SEGMENT_START_SYMBOL='' typeset -g POWERLEVEL9K_RIGHT_PROMPT_LAST_SEGMENT_END_SYMBOL='' typeset -g POWERLEVEL9K_EMPTY_LINE_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL= # ══════════════════════════════════════ # Prompt Char (❯) # ══════════════════════════════════════ typeset -g POWERLEVEL9K_PROMPT_CHAR_OK_{VIINS,VICMD,VIVIS,VIOWR}_FOREGROUND=76 typeset -g POWERLEVEL9K_PROMPT_CHAR_ERROR_{VIINS,VICMD,VIVIS,VIOWR}_FOREGROUND=196 typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIINS_CONTENT_EXPANSION='❯' typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VICMD_CONTENT_EXPANSION='❮' typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIVIS_CONTENT_EXPANSION='V' typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIOWR_CONTENT_EXPANSION='▶' typeset -g POWERLEVEL9K_PROMPT_CHAR_OVERWRITE_STATE=true typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL= typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_PROMPT_FIRST_SEGMENT_START_SYMBOL= # ══════════════════════════════════════ # OS Icon # ══════════════════════════════════════ typeset -g POWERLEVEL9K_OS_ICON_FOREGROUND=255 typeset -g POWERLEVEL9K_OS_ICON_BACKGROUND=24 # ══════════════════════════════════════ # Directory # ══════════════════════════════════════ typeset -g POWERLEVEL9K_DIR_FOREGROUND=255 typeset -g POWERLEVEL9K_DIR_BACKGROUND=31 typeset -g POWERLEVEL9K_DIR_SHORTENED_FOREGROUND=153 typeset -g POWERLEVEL9K_DIR_ANCHOR_FOREGROUND=255 typeset -g POWERLEVEL9K_DIR_ANCHOR_BOLD=true typeset -g POWERLEVEL9K_SHORTEN_STRATEGY=truncate_to_unique typeset -g POWERLEVEL9K_SHORTEN_DIR_LENGTH=3 typeset -g POWERLEVEL9K_SHORTEN_DELIMITER= typeset -g POWERLEVEL9K_DIR_MAX_LENGTH=80 typeset -g POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS=40 typeset -g POWERLEVEL9K_DIR_HYPERLINK=false typeset -g POWERLEVEL9K_DIR_SHOW_WRITABLE=v3 typeset -g POWERLEVEL9K_DIR_NOT_WRITABLE_FOREGROUND=220 typeset -g POWERLEVEL9K_DIR_NOT_WRITABLE_BACKGROUND=88 typeset -g POWERLEVEL9K_DIR_CLASSES=() # ══════════════════════════════════════ # VCS (Git) # ══════════════════════════════════════ typeset -g POWERLEVEL9K_VCS_CLEAN_FOREGROUND=0 typeset -g POWERLEVEL9K_VCS_CLEAN_BACKGROUND=76 typeset -g POWERLEVEL9K_VCS_MODIFIED_FOREGROUND=0 typeset -g POWERLEVEL9K_VCS_MODIFIED_BACKGROUND=178 typeset -g POWERLEVEL9K_VCS_UNTRACKED_FOREGROUND=0 typeset -g POWERLEVEL9K_VCS_UNTRACKED_BACKGROUND=166 typeset -g POWERLEVEL9K_VCS_CONFLICTED_FOREGROUND=255 typeset -g POWERLEVEL9K_VCS_CONFLICTED_BACKGROUND=196 typeset -g POWERLEVEL9K_VCS_LOADING_FOREGROUND=244 typeset -g POWERLEVEL9K_VCS_LOADING_BACKGROUND=240 typeset -g POWERLEVEL9K_VCS_BRANCH_ICON='\uF126 ' typeset -g POWERLEVEL9K_VCS_COMMIT_ICON='@' typeset -g POWERLEVEL9K_VCS_{STAGED,UNSTAGED,UNTRACKED,CONFLICTED,COMMITS_AHEAD,COMMITS_BEHIND}_MAX_NUM=-1 typeset -g POWERLEVEL9K_VCS_VISUAL_IDENTIFIER_EXPANSION= typeset -g POWERLEVEL9K_VCS_BACKENDS=(git) typeset -g POWERLEVEL9K_VCS_DISABLED_WORKDIR_PATTERN='~' # ══════════════════════════════════════ # Status (Exit Code) # ══════════════════════════════════════ typeset -g POWERLEVEL9K_STATUS_EXTENDED_STATES=true # OK — hidden by default (no noise) typeset -g POWERLEVEL9K_STATUS_OK=false typeset -g POWERLEVEL9K_STATUS_OK_FOREGROUND=70 typeset -g POWERLEVEL9K_STATUS_OK_BACKGROUND=238 typeset -g POWERLEVEL9K_STATUS_OK_VISUAL_IDENTIFIER_EXPANSION='✔' # OK pipe typeset -g POWERLEVEL9K_STATUS_OK_PIPE=true typeset -g POWERLEVEL9K_STATUS_OK_PIPE_FOREGROUND=70 typeset -g POWERLEVEL9K_STATUS_OK_PIPE_BACKGROUND=238 typeset -g POWERLEVEL9K_STATUS_OK_PIPE_VISUAL_IDENTIFIER_EXPANSION='✔' # Error typeset -g POWERLEVEL9K_STATUS_ERROR=true typeset -g POWERLEVEL9K_STATUS_ERROR_FOREGROUND=255 typeset -g POWERLEVEL9K_STATUS_ERROR_BACKGROUND=124 typeset -g POWERLEVEL9K_STATUS_ERROR_VISUAL_IDENTIFIER_EXPANSION='✘' # Error signal typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL=true typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_FOREGROUND=255 typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_BACKGROUND=124 typeset -g POWERLEVEL9K_STATUS_VERBOSE_SIGNAME=false typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_VISUAL_IDENTIFIER_EXPANSION='✘' # Error pipe typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE=true typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_FOREGROUND=255 typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_BACKGROUND=124 typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_VISUAL_IDENTIFIER_EXPANSION='✘' # ══════════════════════════════════════ # Command Execution Time # ══════════════════════════════════════ typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_THRESHOLD=3 typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_PRECISION=0 typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FOREGROUND=248 typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_BACKGROUND=240 typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FORMAT='d h m s' typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_VISUAL_IDENTIFIER_EXPANSION= # ══════════════════════════════════════ # Background Jobs # ══════════════════════════════════════ typeset -g POWERLEVEL9K_BACKGROUND_JOBS_FOREGROUND=37 typeset -g POWERLEVEL9K_BACKGROUND_JOBS_BACKGROUND=238 typeset -g POWERLEVEL9K_BACKGROUND_JOBS_VERBOSE=false # ══════════════════════════════════════ # Direnv # ══════════════════════════════════════ typeset -g POWERLEVEL9K_DIRENV_FOREGROUND=178 typeset -g POWERLEVEL9K_DIRENV_BACKGROUND=238 # ══════════════════════════════════════ # Go Version # ══════════════════════════════════════ typeset -g POWERLEVEL9K_GO_VERSION_FOREGROUND=255 typeset -g POWERLEVEL9K_GO_VERSION_BACKGROUND=37 typeset -g POWERLEVEL9K_GO_VERSION_PROJECT_ONLY=true typeset -g POWERLEVEL9K_GO_VERSION_VISUAL_IDENTIFIER_EXPANSION='󰟓' # ══════════════════════════════════════ # Node Version # ══════════════════════════════════════ typeset -g POWERLEVEL9K_NODE_VERSION_FOREGROUND=255 typeset -g POWERLEVEL9K_NODE_VERSION_BACKGROUND=70 typeset -g POWERLEVEL9K_NODE_VERSION_PROJECT_ONLY=true typeset -g POWERLEVEL9K_NODE_VERSION_VISUAL_IDENTIFIER_EXPANSION='󰎙' # ══════════════════════════════════════ # Context (user@hostname) # ══════════════════════════════════════ typeset -g POWERLEVEL9K_CONTEXT_FOREGROUND=255 typeset -g POWERLEVEL9K_CONTEXT_BACKGROUND=240 typeset -g POWERLEVEL9K_CONTEXT_ROOT_FOREGROUND=255 typeset -g POWERLEVEL9K_CONTEXT_ROOT_BACKGROUND=124 typeset -g POWERLEVEL9K_CONTEXT_{DEFAULT,SUDO}_{CONTENT_EXPANSION,TEMPLATE}='%n@%m' typeset -g POWERLEVEL9K_CONTEXT_ROOT_TEMPLATE='%B%n@%m' # Only show in SSH sessions or as root typeset -g POWERLEVEL9K_CONTEXT_{DEFAULT,SUDO}_CONTENT_EXPANSION= typeset -g POWERLEVEL9K_ALWAYS_SHOW_CONTEXT=false typeset -g POWERLEVEL9K_CONTEXT_{REMOTE,REMOTE_SUDO}_CONTENT_EXPANSION='%n@%m' # ══════════════════════════════════════ # Time # ══════════════════════════════════════ typeset -g POWERLEVEL9K_TIME_FOREGROUND=255 typeset -g POWERLEVEL9K_TIME_BACKGROUND=236 typeset -g POWERLEVEL9K_TIME_FORMAT='%D{%H:%M}' typeset -g POWERLEVEL9K_TIME_UPDATE_ON_COMMAND=false typeset -g POWERLEVEL9K_TIME_VISUAL_IDENTIFIER_EXPANSION= # ══════════════════════════════════════ # Transient Prompt # ══════════════════════════════════════ # Replace previous prompts with a minimal version to keep terminal clean typeset -g POWERLEVEL9K_TRANSIENT_PROMPT=always # ══════════════════════════════════════ # Instant Prompt # ══════════════════════════════════════ # Show prompt immediately while plugins load in background typeset -g POWERLEVEL9K_INSTANT_PROMPT=verbose # ══════════════════════════════════════ # Hot Reload # ══════════════════════════════════════ (( ! $+functions[p10k] )) || p10k reload } (( ${#p10k_config_opts} )) && setopt ${p10k_config_opts[@]} 'builtin' 'unset' 'p10k_config_opts' EOFP10K chown "${INSTALL_USER}:${INSTALL_USER}" "$p10k_file" log_success ".p10k.zsh generated (Rainbow + Nerd Font v3) ✓" log_info "To reconfigure interactively: ${C_BOLD}p10k configure${C_NC}" } #=============================================================================== # Nerd Font Installation #=============================================================================== install_nerd_fonts() { section_header "🔤 Nerd Font: ${NERD_FONT}" ensure_deps curl jq unzip fontconfig || return 1 local font_dir="${USER_HOME}/.local/share/fonts/NerdFonts" # --- Check if already installed --- if [[ -d "$font_dir" ]] && compgen -G "${font_dir}/*.ttf" >/dev/null 2>&1; then local count count=$(find "$font_dir" -name '*.ttf' 2>/dev/null | wc -l) log_info "Nerd Fonts already installed (${count} files in ${font_dir})" else log_step "Downloading ${NERD_FONT} Nerd Font from GitHub..." local tmp_dir tmp_dir=$(mktemp -d /tmp/nerd-font-XXXXXX) download_github_asset "ryanoasis/nerd-fonts" \ "${NERD_FONT}\\.zip" \ "${tmp_dir}/${NERD_FONT}.zip" || { log_error "Failed to download Nerd Font" rm -rf "$tmp_dir" return 1 } run_as_user "mkdir -p '${font_dir}'" unzip -o -q "${tmp_dir}/${NERD_FONT}.zip" '*.ttf' -d "$font_dir" 2>/dev/null || \ unzip -o -q "${tmp_dir}/${NERD_FONT}.zip" -d "$font_dir" 2>/dev/null # Clean up non-essential files rm -f "${font_dir}"/*Windows*.ttf 2>/dev/null || true rm -f "${font_dir}"/*.md "${font_dir}"/*.txt "${font_dir}"/*.json 2>/dev/null || true chown -R "${INSTALL_USER}:${INSTALL_USER}" "${font_dir}" fc-cache -f "${font_dir}" >/dev/null 2>&1 rm -rf "$tmp_dir" local count count=$(find "$font_dir" -name '*.ttf' 2>/dev/null | wc -l) log_success "Installed ${count} font files → ${font_dir}" fi # --- Copy to Windows side (for Windows Terminal) --- if [[ "$IS_WSL" == "true" && -n "${WIN_USER}" ]]; then local win_font_dir="/mnt/c/Users/${WIN_USER}/AppData/Local/Microsoft/Windows/Fonts" if [[ -d "/mnt/c/Users/${WIN_USER}" ]]; then mkdir -p "$win_font_dir" 2>/dev/null || true if cp "${font_dir}"/*.ttf "$win_font_dir/" 2>/dev/null; then log_success "Fonts copied to Windows → ${win_font_dir}" else log_warn "Could not copy fonts to Windows side (non-critical)" fi fi fi echo "" echo -e " ${C_YELLOW}╔══════════════════════════════════════════════════════╗${C_NC}" echo -e " ${C_YELLOW}║${C_NC} ${C_BOLD}📌 Windows Terminal 字体配置:${C_NC} ${C_YELLOW}║${C_NC}" echo -e " ${C_YELLOW}║${C_NC} ${C_YELLOW}║${C_NC}" echo -e " ${C_YELLOW}║${C_NC} Settings → Profiles → Defaults → Appearance ${C_YELLOW}║${C_NC}" echo -e " ${C_YELLOW}║${C_NC} → Font face: ${C_BOLD}JetBrainsMono Nerd Font${C_NC} ${C_YELLOW}║${C_NC}" echo -e " ${C_YELLOW}║${C_NC} ${C_YELLOW}║${C_NC}" echo -e " ${C_YELLOW}║${C_NC} 如字体未出现,请在 Windows 中手动安装: ${C_YELLOW}║${C_NC}" echo -e " ${C_YELLOW}║${C_NC} 双击 .ttf 文件 → Install ${C_YELLOW}║${C_NC}" echo -e " ${C_YELLOW}╚══════════════════════════════════════════════════════╝${C_NC}" echo "" } #=============================================================================== # Modern CLI Tools #=============================================================================== install_modern_cli() { section_header "⚡ Modern CLI Tools" _install_bat _install_eza _install_fd _install_ripgrep _install_fzf _install_zoxide _install_delta _install_lazygit echo "" log_success "Modern CLI tools installation complete ✓" } # ── bat (better cat) ── _install_bat() { if check_cmd bat || check_cmd batcat; then log_info "bat — already installed ✓" return 0 fi log_step "Installing bat..." if version_gte "$UBUNTU_VERSION" "22.04"; then apt-get install -y -qq bat >/dev/null 2>&1 # Create symlink: batcat → bat [[ -f /usr/bin/batcat ]] && ln -sf /usr/bin/batcat /usr/local/bin/bat 2>/dev/null || true else case "$ARCH" in amd64) install_github_deb "sharkdp/bat" "bat_.*_amd64\\.deb" ;; arm64) install_github_deb "sharkdp/bat" "bat_.*_arm64\\.deb" ;; esac fi if check_cmd bat || check_cmd batcat; then log_success "bat installed ✓" else log_warn "bat — installation failed" fi } # ── eza (better ls) ── _install_eza() { if check_cmd eza; then log_info "eza — already installed ✓" return 0 fi log_step "Installing eza..." if version_gte "$UBUNTU_VERSION" "24.04"; then apt-get install -y -qq eza >/dev/null 2>&1 else # Install from GitHub release case "$ARCH" in amd64) install_github_tar_binary "eza-community/eza" "eza_x86_64-unknown-linux-gnu\\.tar\\.gz" "eza" ;; arm64) install_github_tar_binary "eza-community/eza" "eza_aarch64-unknown-linux-gnu\\.tar\\.gz" "eza" ;; esac fi if check_cmd eza; then log_success "eza installed ✓" else log_warn "eza — installation failed" fi } # ── fd (better find) ── _install_fd() { if check_cmd fd || check_cmd fdfind; then log_info "fd — already installed ✓" return 0 fi log_step "Installing fd-find..." apt-get install -y -qq fd-find >/dev/null 2>&1 || { case "$ARCH" in amd64) install_github_deb "sharkdp/fd" "fd_.*_amd64\\.deb" ;; arm64) install_github_deb "sharkdp/fd" "fd_.*_arm64\\.deb" ;; esac } # Create symlink: fdfind → fd [[ -f /usr/bin/fdfind ]] && ln -sf /usr/bin/fdfind /usr/local/bin/fd 2>/dev/null || true if check_cmd fd || check_cmd fdfind; then log_success "fd installed ✓" else log_warn "fd — installation failed" fi } # ── ripgrep (better grep) ── _install_ripgrep() { if check_cmd rg; then log_info "ripgrep — already installed ✓" return 0 fi log_step "Installing ripgrep..." apt-get install -y -qq ripgrep >/dev/null 2>&1 || { case "$ARCH" in amd64) install_github_deb "BurntSushi/ripgrep" "ripgrep_.*_amd64\\.deb" ;; arm64) install_github_deb "BurntSushi/ripgrep" "ripgrep_.*_arm64\\.deb" ;; esac } if check_cmd rg; then log_success "ripgrep installed ✓" else log_warn "ripgrep — installation failed" fi } # ── fzf (fuzzy finder) ── _install_fzf() { if check_cmd fzf; then log_info "fzf — already installed ✓" return 0 fi log_step "Installing fzf..." # Try apt first for newer Ubuntu if version_gte "$UBUNTU_VERSION" "22.04"; then apt-get install -y -qq fzf >/dev/null 2>&1 fi # Fallback to git installation (gets latest version + shell integrations) if ! check_cmd fzf; then run_as_user "git clone --depth 1 https://github.com/junegunn/fzf.git '${USER_HOME}/.fzf'" 2>/dev/null || true if [[ -d "${USER_HOME}/.fzf" ]]; then run_as_user "'${USER_HOME}/.fzf/install' --all --no-update-rc --no-bash --no-zsh" 2>/dev/null || true # Add to path ln -sf "${USER_HOME}/.fzf/bin/fzf" /usr/local/bin/fzf 2>/dev/null || true fi fi if check_cmd fzf; then log_success "fzf installed ✓" else log_warn "fzf — installation failed" fi } # ── zoxide (better cd) ── _install_zoxide() { if check_cmd zoxide; then log_info "zoxide — already installed ✓" return 0 fi log_step "Installing zoxide..." if version_gte "$UBUNTU_VERSION" "24.04"; then apt-get install -y -qq zoxide >/dev/null 2>&1 else case "$ARCH" in amd64) install_github_deb "ajeetdsouza/zoxide" "zoxide_.*_amd64\\.deb" ;; arm64) install_github_deb "ajeetdsouza/zoxide" "zoxide_.*_arm64\\.deb" ;; esac fi if check_cmd zoxide; then log_success "zoxide installed ✓" else log_warn "zoxide — installation failed" fi } # ── delta (better diff / git pager) ── _install_delta() { if check_cmd delta; then log_info "delta — already installed ✓" return 0 fi log_step "Installing git-delta..." case "$ARCH" in amd64) install_github_deb "dandavison/delta" "git-delta_.*_amd64\\.deb" ;; arm64) install_github_deb "dandavison/delta" "git-delta_.*_arm64\\.deb" ;; esac # Configure git to use delta if check_cmd delta; then run_as_user "git config --global core.pager delta" 2>/dev/null || true run_as_user "git config --global interactive.diffFilter 'delta --color-only'" 2>/dev/null || true run_as_user "git config --global delta.navigate true" 2>/dev/null || true run_as_user "git config --global delta.light false" 2>/dev/null || true run_as_user "git config --global delta.line-numbers true" 2>/dev/null || true run_as_user "git config --global merge.conflictstyle diff3" 2>/dev/null || true run_as_user "git config --global diff.colorMoved default" 2>/dev/null || true log_success "delta installed + git configured ✓" else log_warn "delta — installation failed" fi } # ── lazygit (git TUI) ── _install_lazygit() { if check_cmd lazygit; then log_info "lazygit — already installed ✓" return 0 fi log_step "Installing lazygit..." case "$ARCH" in amd64) install_github_tar_binary "jesseduffield/lazygit" "lazygit_.*_linux_x86_64\\.tar\\.gz" "lazygit" ;; arm64) install_github_tar_binary "jesseduffield/lazygit" "lazygit_.*_linux_arm64\\.tar\\.gz" "lazygit" ;; esac if check_cmd lazygit; then log_success "lazygit installed ✓" else log_warn "lazygit — installation failed" fi } #=============================================================================== # Go Installation #=============================================================================== install_golang() { section_header "🐹 Go Installation" ensure_deps curl jq || return 1 # --- Resolve version --- local target_version="$GO_VERSION" if [[ "$target_version" == "latest" ]]; then log_step "Detecting latest Go version..." target_version=$(curl -sL --connect-timeout 15 'https://golang.google.cn/dl/?mode=json' \ | jq -r '.[0].version' 2>/dev/null \ | sed 's/^go//') if [[ -z "$target_version" || "$target_version" == "null" ]]; then # Fallback to go.dev target_version=$(curl -sL --connect-timeout 15 'https://go.dev/dl/?mode=json' \ | jq -r '.[0].version' 2>/dev/null \ | sed 's/^go//') fi if [[ -z "$target_version" || "$target_version" == "null" ]]; then log_error "Failed to detect latest Go version" return 1 fi fi log_info "Target version: Go ${target_version}" # --- Check current installation --- if check_cmd go; then local current current=$(go version 2>/dev/null | grep -oP 'go\K[0-9.]+' || echo "") if [[ "$current" == "$target_version" ]]; then log_success "Go ${target_version} already installed ✓" _setup_go_env return 0 fi log_info "Current Go: ${current} → Upgrading to ${target_version}" fi # --- Download --- local go_tarball="go${target_version}.linux-${GO_ARCH}.tar.gz" local download_url="https://golang.google.cn/dl/${go_tarball}" local tmp_file="/tmp/${go_tarball}" log_step "Downloading: ${go_tarball}" if ! curl -fsSL --connect-timeout 30 --retry 3 "$download_url" -o "$tmp_file"; then # Fallback to go.dev download_url="https://go.dev/dl/${go_tarball}" log_step "Retrying from go.dev..." curl -fsSL --connect-timeout 30 --retry 3 "$download_url" -o "$tmp_file" || { log_error "Failed to download Go" return 1 } fi # --- Install --- log_step "Installing to /usr/local/go..." rm -rf /usr/local/go tar -C /usr/local -xzf "$tmp_file" rm -f "$tmp_file" # --- Create GOPATH --- run_as_user "mkdir -p '${USER_HOME}/go/bin' '${USER_HOME}/go/src' '${USER_HOME}/go/pkg'" # --- Setup environment --- _setup_go_env # --- Verify --- local installed_version installed_version=$(/usr/local/go/bin/go version 2>/dev/null | grep -oP 'go\K[0-9.]+' || echo "unknown") log_success "Go ${installed_version} installed ✓" log_info "GOROOT=/usr/local/go GOPATH=\${HOME}/go" log_info "GOPROXY=https://goproxy.cn,direct" } _setup_go_env() { # System-wide (all users, all login shells) cat > /etc/profile.d/golang.sh << 'EOFGO' # Go environment — managed by wsl-dev-setup.sh export GOROOT=/usr/local/go export GOPATH="${HOME}/go" export PATH="${GOROOT}/bin:${GOPATH}/bin:${PATH}" export GOPROXY=https://goproxy.cn,direct export GONOSUMDB="*" EOFGO chmod 644 /etc/profile.d/golang.sh # .bashrc local go_bashrc=' if [[ -d /usr/local/go ]]; then export GOROOT=/usr/local/go export GOPATH="${HOME}/go" export PATH="${GOROOT}/bin:${GOPATH}/bin:${PATH}" export GOPROXY=https://goproxy.cn,direct export GONOSUMDB="*" fi' add_to_rc "${USER_HOME}/.bashrc" "GOLANG" "$go_bashrc" # .profile (for sh / login shells) local go_profile=' if [ -d /usr/local/go ]; then export GOROOT=/usr/local/go export GOPATH="${HOME}/go" export PATH="${GOROOT}/bin:${GOPATH}/bin:${PATH}" export GOPROXY=https://goproxy.cn,direct export GONOSUMDB="*" fi' add_to_rc "${USER_HOME}/.profile" "GOLANG" "$go_profile" # .zshrc already includes Go config via _generate_zshrc log_step "Go env configured: /etc/profile.d, .bashrc, .profile, .zshrc" } #=============================================================================== # Node.js Installation (via fnm) #=============================================================================== install_nodejs() { section_header "📗 Node.js LTS via fnm (Fast Node Manager)" ensure_deps curl jq unzip || return 1 local fnm_dir="${USER_HOME}/.local/share/fnm" local fnm_bin="${fnm_dir}/fnm" # --- Install fnm --- if [[ -x "$fnm_bin" ]]; then log_info "fnm already installed ✓" else log_step "Installing fnm from GitHub..." local tmp_dir tmp_dir=$(mktemp -d /tmp/fnm-XXXXXX) local fnm_pattern case "$ARCH" in amd64) fnm_pattern="fnm-linux\\.zip" ;; arm64) fnm_pattern="fnm-arm64\\.zip" ;; esac download_github_asset "Schniz/fnm" "$fnm_pattern" "${tmp_dir}/fnm.zip" || { log_error "Failed to download fnm" rm -rf "$tmp_dir" return 1 } run_as_user "mkdir -p '${fnm_dir}'" unzip -o -q "${tmp_dir}/fnm.zip" -d "$fnm_dir" chmod +x "$fnm_bin" chown -R "${INSTALL_USER}:${INSTALL_USER}" "$fnm_dir" # Also link to /usr/local/bin for root access ln -sf "$fnm_bin" /usr/local/bin/fnm 2>/dev/null || true rm -rf "$tmp_dir" log_success "fnm installed → ${fnm_dir}" fi # --- Setup environment for fnm --- _setup_fnm_env # --- Install Node.js LTS --- log_step "Installing Node.js LTS..." # We need fnm in the PATH for the user, and specify shell bash to prevent "Can't infer shell!" warning local install_cmd="export PATH='${fnm_dir}:\${PATH}'; eval \"\$(fnm env --shell bash)\"; fnm install --lts" if ! run_as_user "$install_cmd"; then log_warn "First attempt to install Node.js LTS failed. Clearing partial downloads and retrying..." run_as_user "rm -rf '${fnm_dir}/node-versions/.downloads'" 2>/dev/null || true if ! run_as_user "$install_cmd"; then log_error "Failed to install Node.js LTS" return 1 fi fi run_as_user "export PATH='${fnm_dir}:\${PATH}'; eval \"\$(fnm env --shell bash)\"; fnm default lts-latest" 2>/dev/null || true # --- Get installed version --- local node_version node_version=$(run_as_user "export PATH='${fnm_dir}:\${PATH}'; eval \"\$(fnm env --shell bash)\"; node --version" 2>/dev/null || echo "unknown") local npm_version npm_version=$(run_as_user "export PATH='${fnm_dir}:\${PATH}'; eval \"\$(fnm env --shell bash)\"; npm --version" 2>/dev/null || echo "unknown") log_success "Node.js ${node_version} (npm ${npm_version}) installed ✓" # --- Set npm China mirror --- log_step "Setting npm registry → npmmirror.com" run_as_user "export PATH='${fnm_dir}:\${PATH}'; eval \"\$(fnm env --shell bash)\"; npm config set registry https://registry.npmmirror.com" 2>/dev/null || true # --- Install global npm tools --- log_step "Installing tldr..." run_as_user "export PATH='${fnm_dir}:\${PATH}'; eval \"\$(fnm env --shell bash)\"; npm install -g tldr" 2>/dev/null || { log_warn "tldr installation failed (non-critical)" } log_success "Node.js environment configured ✓" log_info "npm registry: https://registry.npmmirror.com" } _setup_fnm_env() { local fnm_dir="${USER_HOME}/.local/share/fnm" # .bashrc local fnm_bashrc=" if [[ -d \"${fnm_dir}\" ]]; then export FNM_DIR=\"${fnm_dir}\" export PATH=\"\${FNM_DIR}:\${PATH}\" eval \"\$(fnm env --use-on-cd --shell bash)\" fi" add_to_rc "${USER_HOME}/.bashrc" "FNM-NODEJS" "$fnm_bashrc" # .profile local fnm_profile=" if [ -d \"${fnm_dir}\" ]; then export FNM_DIR=\"${fnm_dir}\" export PATH=\"\${FNM_DIR}:\${PATH}\" eval \"\$(fnm env --use-on-cd)\" fi" add_to_rc "${USER_HOME}/.profile" "FNM-NODEJS" "$fnm_profile" # .zshrc already includes fnm config via _generate_zshrc log_step "fnm env configured: .bashrc, .profile, .zshrc" } #=============================================================================== # Verification & Summary #=============================================================================== print_summary() { local elapsed=$(( $(date +%s) - SCRIPT_START_TIME )) local minutes=$(( elapsed / 60 )) local seconds=$(( elapsed % 60 )) echo "" echo -e "${C_CYAN}╔══════════════════════════════════════════════════════════════╗${C_NC}" echo -e "${C_CYAN}║${C_NC} ${C_BOLD}📊 Installation Summary${C_NC} ${C_CYAN}║${C_NC}" echo -e "${C_CYAN}╠══════════════════════════════════════════════════════════════╣${C_NC}" # System info printf "${C_CYAN}║${C_NC} %-58s${C_CYAN}║${C_NC}\n" "🖥️ Ubuntu ${UBUNTU_VERSION} (${UBUNTU_CODENAME}) — ${ARCH}" [[ "$IS_WSL" == "true" ]] && \ printf "${C_CYAN}║${C_NC} %-58s${C_CYAN}║${C_NC}\n" "📦 WSL2 — Windows user: ${WIN_USER:-N/A}" echo -e "${C_CYAN}╠──────────────────────────────────────────────────────────────╣${C_NC}" # Module results _check_result "APT Mirror" "[[ -n '${APT_MIRROR}' ]]" _check_result "Locale" "locale 2>/dev/null | grep -q 'UTF-8'" _check_result "curl" "check_cmd curl" _check_result "git" "check_cmd git" _check_result "wget" "check_cmd wget" _check_result "ping" "check_cmd ping" _check_result "mtr" "check_cmd mtr" _check_result "Zsh" "check_cmd zsh" _check_result "Zinit" "[[ -d '${USER_HOME}/.local/share/zinit/zinit.git' ]]" _check_result "P10k Config" "[[ -f '${USER_HOME}/.p10k.zsh' ]]" _check_result "Nerd Font" "compgen -G '${USER_HOME}/.local/share/fonts/NerdFonts/*.ttf' >/dev/null 2>&1" _check_result "bat" "check_cmd bat || check_cmd batcat" _check_result "eza" "check_cmd eza" _check_result "fd" "check_cmd fd || check_cmd fdfind" _check_result "ripgrep" "check_cmd rg" _check_result "fzf" "check_cmd fzf" _check_result "zoxide" "check_cmd zoxide" _check_result "delta" "check_cmd delta" _check_result "lazygit" "check_cmd lazygit" _check_result "Go" "check_cmd go || [[ -x /usr/local/go/bin/go ]]" _check_result "fnm" "[[ -x '${USER_HOME}/.local/share/fnm/fnm' ]]" _check_result "Node.js" "run_as_user \"export PATH='${USER_HOME}/.local/share/fnm:\${PATH}'; eval \\\"\\\$(fnm env)\\\"; node --version\" >/dev/null 2>&1" echo -e "${C_CYAN}╠──────────────────────────────────────────────────────────────╣${C_NC}" # Versions local go_ver="N/A" [[ -x /usr/local/go/bin/go ]] && go_ver=$(/usr/local/go/bin/go version 2>/dev/null | grep -oP 'go\K[0-9.]+' || echo "N/A") local node_ver="N/A" node_ver=$(run_as_user "export PATH='${USER_HOME}/.local/share/fnm:\${PATH}'; eval \"\$(fnm env)\" 2>/dev/null; node --version" 2>/dev/null || echo "N/A") printf "${C_CYAN}║${C_NC} %-58s${C_CYAN}║${C_NC}\n" "Go: ${go_ver} Node.js: ${node_ver}" printf "${C_CYAN}║${C_NC} %-58s${C_CYAN}║${C_NC}\n" "⏱️ Completed in ${minutes}m ${seconds}s" if [[ -n "$FAILED_MODULES" ]]; then echo -e "${C_CYAN}╠──────────────────────────────────────────────────────────────╣${C_NC}" printf "${C_CYAN}║${C_NC} ${C_RED}⚠️ Failed modules: %-39s${C_NC}${C_CYAN}║${C_NC}\n" "$FAILED_MODULES" fi echo -e "${C_CYAN}╚══════════════════════════════════════════════════════════════╝${C_NC}" # Post-install instructions echo "" echo -e "${C_BOLD}📌 Next Steps:${C_NC}" echo -e " 1. Set Windows Terminal font: ${C_BOLD}JetBrainsMono Nerd Font${C_NC}" echo -e " 2. Restart WSL: ${C_DIM}wsl --shutdown${C_NC} then reopen terminal" echo -e " 3. (Optional) Reconfigure prompt: ${C_DIM}p10k configure${C_NC}" echo -e " 4. (Optional) Toggle proxy: ${C_DIM}proxy_on${C_NC} / ${C_DIM}proxy_off${C_NC}" echo "" } _check_result() { local label="$1" local test_cmd="$2" if eval "$test_cmd" 2>/dev/null; then printf "${C_CYAN}║${C_NC} ${C_GREEN}✅${C_NC} %-55s${C_CYAN}║${C_NC}\n" "$label" else printf "${C_CYAN}║${C_NC} ${C_RED}❌${C_NC} %-55s${C_CYAN}║${C_NC}\n" "$label" fi } #=============================================================================== # Main Entry Point #=============================================================================== main() { parse_args "$@" # --- Root check --- if [[ "$(id -u)" -ne 0 ]]; then log_error "This script must be run as root." echo "" echo -e " Usage: ${C_BOLD}sudo bash ${SCRIPT_NAME}${C_NC}" echo "" exit 1 fi # --- Banner --- print_banner # --- Dry run --- if [[ "$DRY_RUN" == "true" ]]; then log_info "DRY RUN — showing what would be executed:" for module in $ALL_MODULES; do if should_run_module "$module"; then echo -e " ${C_GREEN}▶${C_NC} $module" else echo -e " ${C_DIM}▷ $module (skipped)${C_NC}" fi done exit 0 fi # ── Phase 0: System Detection ── detect_system # ── Phase 1: Network & Sources (needed for downloads) ── if should_run_module "proxy" && [[ "$ENABLE_PROXY" == "true" ]]; then setup_proxy || FAILED_MODULES="${FAILED_MODULES} proxy" fi if should_run_module "mirror"; then setup_apt_mirror || FAILED_MODULES="${FAILED_MODULES} mirror" fi # ── Phase 2: System Foundation ── if should_run_module "locale"; then setup_locale || FAILED_MODULES="${FAILED_MODULES} locale" fi if should_run_module "base-tools"; then install_base_tools || FAILED_MODULES="${FAILED_MODULES} base-tools" fi # ── Phase 3: Terminal Environment ── if should_run_module "zsh"; then install_zsh_terminal || FAILED_MODULES="${FAILED_MODULES} zsh" fi if should_run_module "fonts"; then install_nerd_fonts || FAILED_MODULES="${FAILED_MODULES} fonts" fi if should_run_module "modern-cli"; then install_modern_cli || FAILED_MODULES="${FAILED_MODULES} modern-cli" fi # ── Phase 4: Development Languages ── if should_run_module "golang"; then install_golang || FAILED_MODULES="${FAILED_MODULES} golang" fi if should_run_module "nodejs"; then install_nodejs || FAILED_MODULES="${FAILED_MODULES} nodejs" fi # ── Phase 5: Summary ── print_summary } # ── Execute ── main "$@"